Witam!
Jak już kiedyś pisałem na tym forum, najważniejszą kwestią w programowaniu wielowątkowym jest synchronizacja dostępu do obszaru pamięci współdzielonej z innymi wątkami potomnymi jak również macierzystym. Nie jestem przekonany czy podane powyżej rozwiązanie zapewnia odpowiedni poziom zabezpieczenia synchronizacji. Pokaże inne rozwiązanie, niekoniecznie łatwiejsze, i krótsze, ale pewne. Jeżeli ktoś koniecznie chce używać metod
BeginThread itp. to może coś poczytać o synchronizacji, może modyfikator
__thread będzie przydatny, tego nie wiem dokładnie.
W tym problemie wątek odwołuje się do pamięci współdzielonej (zmienna i, pole Edit, obiekt formatki), co może być niebezpieczne i w każdym momencie może spowodować wyjątek!!!
Borland posiada specjalną klasę
TThread, która może być użyta do konstrukcji nowych wątków i dostarcza odpowiednich mechanizmów zabezpieczenia pamięci.
Do poprawnego użycia klasy wystarczy zdefiniować konstruktor oraz funkcję
Execute, która jest funkcją czysto wirtualną, a której instrukcję są wykonywane jako nowy wątek. Jeżeli nie zamierzamy naszej klasy dziedziczyć dalej to najprostsza definicja klasy może wyglądać np. tak:
- Kod: Zaznacz cały
class myThread : public TThread
{
void __fastcall Execute(void);
public:
myThread(void):TThread(true){}
};
Teraz w dowolnym pliku .cpp dołączamy plik nagłówkowy z powyższą definicją i definiujemy funkcję
Execute:
- Kod: Zaznacz cały
void __fastcall myThread::Execute(void)
{
// Instrukje wykonywane jako osobny wątek
}
Aby uruchomić dany wątek wystarczą dwie linijki:
- Kod: Zaznacz cały
myThread* t = new myThread;
t->Resume();
Teraz to o co mi chodzi: synchronizacja pamięci. Do tego celu należy użyć funkcji
Synchronize, która jako parametr przyjmuje wskaźnik do funkcji będącej metodą klasy, która jest typu void i nie przyjmuje żadnych parametrów. Tak więc aby poprawnie i bezpiecznie można było pobrać wartość i należy zdefiniować odpowiednią do tego funkcje (np. get_i) i wywołać ją wewnątrz ciała funkcji
Execute przy pomocy funkcji
Synchronize. Trzeba też zadbać o miejsce zapisania pobranej wartości, czyli w definicji ciała klasy definiujemy nowe pole niech będzie też
i, teraz definicja klasy będzie następująca:
- Kod: Zaznacz cały
class myThread : public TThread
{
int i;
void __fastcall get_i(void);
void __fastcall Execute(void);
public:
myThread(void):TThread(true){}
};
definicja ciała funkcji
get_i powinna wyglądać np. tak:
- Kod: Zaznacz cały
void __fastcall myThread::get_i(void)
{
i = Form1->i;
}
natomiast w funkcji
Execute należy umieścić instrukcję:
- Kod: Zaznacz cały
Synchronize(get_i);
Po wywołaniu tej metody wartość pola
i należącego do obiektu Form będzie skopiowana do pola należącego do obiektu wątku w sposób bezpieczny. Takie pobieranie wartości zmiennej jest tylko przykładem, prostszym sposobem jest przekazanie tej wartości do obiektu wątku przed uruchomieniem za pomocą odpowiednio zdefiniowanego konstruktora, lub też specjalnej metody np:
- Kod: Zaznacz cały
myThread(int _i):TThread(true), i(_i){}
wtedy utworzenie obiektu będzie wyglądało następująco:
- Kod: Zaznacz cały
myThread* y = new myThread(Form1->i);
y->Resume();
Takie przekazanie wartości jest jak najbardziej bezpieczne gdyż wątek jeszcze nie jest uruchomiony, a co następuje w kolejnej linii. Do zmiennych zdefiniowanych wewnątrz klasy
myThread wątek może odwoływać się bezpiecznie.
Najważniejsze dla tego przykładu jest odpowiednie zdefiniowanie funkcji wyświetlania w polu Edit: definiujemy funkcję np.
display_value która będzie wyświetlać daną zmienną w polu Edit. Tak dla uogólnienia nie będziemy wyświetlać wartości pola
i bo wątpię aby to było celem autora tego postu. Wyświetlać będziemy inną zmienną która tylko będzie przyjmować wartość i ale też może dowolnie inną. W tym celu definiujemy nowe pole w klasie np.
value, czyli nasza klasa z nowym konstruktorem będzie wyglądać tak:
- Kod: Zaznacz cały
class myThread : public TThread
{
int i;
int value;
void __fastcall display_value(void);
void __fastcall Execute(void);
public:
myThread(int _i):TThread(true), i(_i){}
};
natomiast definicja funckji
display_value tak:
- Kod: Zaznacz cały
void __fastcall myThread::display_value(void)
{
Form1->Edit1->Text = Form1->Edit1->Text + String(value);
}
oraz ciało funkcji Execute:
- Kod: Zaznacz cały
void __fastcall myThread::Execute(void)
{
for(;i>=0;--i)
{
value = i;
Synchronize(display_value);
}
}
Jeszcze raz powtarzam odpowiednie zabezpieczenie przed wielodostępem do pamięci współdzielonej jest najważniejsze w programowaniu wielowątkowym. Przedstawiony sposób jest pewnie jednym z wielu. Nie wiem czy Borland oferuje coś w rodzaju mutex-ów, albo semaforów które może w bardziej prosty sposób pasował by do pierwotnego przykładu autora tego postu. Można ewentualnie poszukać odpowiednich bibliotek.
Jeżeli ktoś mi nie wież jak ważne jest zabezpieczenie przed wielodostępem, to niech przerobi podany przeze mnie przykład na taki aby w funkcji
Execute znajdowały się instrukcje np. rysowania np. na Canvas. Program się zaraz wyłoży. Dla przykładu, jeżeli już mamy gotową klasę jak przedstawiłem, to w funkcji
Execute można umieścić taki oto kod:
- Kod: Zaznacz cały
for(int h=0;h<Form1->ClientHeight;++h)
for(int w=0;w<Form1->ClientWidth;++w)
Form1->Canvas->Pixels[w][h] = RGB(w,h,0);
i dla różnicy ten sam kod w funkcji
display_value i wywołać w funkcji
Execute zawierającą instrukcję:
- Kod: Zaznacz cały
Synchronize(display_value)
.
Jak pisałem program nie zawsze generuje wyjątek, wtedy można kilka razy powtórzyć operację, albo nawet próbować na innych komputerach.
Aha, wywołanie funkcji
Synchronize powoduje że aplikacja się zachowuje jak jednowątkowa, dlatego w metodach wywoływanych przez tą funkcję trzeba umieszczać jak najmniejszą ilość instrukcji, najlepiej tylko te odwołujące się do zasobów współdzielonych, tak aby wątki mogły się jak najdłużej wykonywać równolegle.
Mam nadzieje że ten post będzie przydatny!
Pozdr! i powodzenia!
If a machine is expected to be infallible, it cannot also be intelligent.
-- A.Turing