List od Marcina Sikorskiego (19 XI 2007)
----------------------------------------
Wielu programistów korzystających z klasy TThread w VCL (Delphi
lub Borland C++Builder) popełnia ten sam błąd polegający na zwalnianiu
obiektów wykorzystywanych w osobnym wątku w destruktorze klasy podczas gdy
nie jest oczywiste, że metoda Execute w tym czasie zakończyła już swoje
działanie.
Programiści, pisząc swoją klasę wątku dziedziczącą z TThread tworzą swój destruktor wirtualny,
w którym usuwają wszystkie utworzone w konstruktorze obiekty.
Jest to oczywiście poprawne, ale nie dla wątków.
Kod własnego destruktora wątku wykonywany jest w kontekście
wątku, który wywołuje usunięcie obiektu, zanim wątek ten zakończy swoje działanie.
Warto jednak wiedzieć, że destruktor nie sprawdza w żaden sposób, czy
instrukcje metody Execute zostały już wykonane do końca (jej działanie
nie jest zsychronizowane z wątkiem, który usuwa nasz wątek) - osobny wątek może
nadal działać, a tym samym odwoływać się do usuniętych w destruktorze obiektów.
Metoda Execute wygląda zwykle następująco:
void TMojWatek::Execute()
{
while (!Terminated)
{
instrukcja1
instrukcja2
instrukcja3
instrukcja4
instrukcja5
odwolanie do zasobu
instrukcja6
}
instrukcja7
}
Załóżmy, że destruktor zostanie wykonany w trakcie działania instrukcji
od 1 do 5. Wówczas odwołanie do zasobów stworzonych na potrzeby
wątku zakończy się błędem powodującym zakończenie działania programu
(komunikat "Abnormal program termination").
Łatwo to sprawdzić wstawiając breakpoint w linii "instrukcja7" oraz w
pierwszej linii destruktora ~TMojWatek, a okaże się która linia wykona się
pierwsza.
Jest to spowodowane tym, iż zgodnie z zasadą dziedziczenia najpierw wywołuje się nasz destruktor
a dopiero potem destruktor ~TThread, który zajmuje się zatrzymaniem wątku i oczekiwaniem na jego zakończenie:
Jego kod w przybliżeniu wygląda tak:
~TThread()
{
this->Terminate();
this->WaitFor();
Free();
}
Funkcja Terminate() nie robi nic innego jak tylko ustawia Terminated na true.
Zadaniem wątku (metoda Execute) jest cykliczne sprawdzanie tej zmiennej i ewentualne
zakończenie swojej pracy - i zwolnienie swoich zasobów.
Opisany powyżej problem można rozwiązać na kilka sposobów:
-
Wykorzystać zdarzenie OnTerminate i w nim usuwać wykorzystywane przez
wątek obiekty. (Dokumentacja podaje, że zdarzenie to jest wywoływane
w kontekście wątku głównego aplikacji - VCL, co powoduje, że jest ono
całkowicie bezpieczne, nawet jeśli kod odnosi się do obiektów VCL).
-
Usunąć je na końcu funkcji Execute (w przykładzie: za linią "instrukcja7").
Z poważaniem
Marcin Sikorski