Obliczenia współbieżne na wątkach
Kiedy można stosować obliczenia współbieżne?
Aby obliczenia współbieżne były możliwe do zastosowania, musi istnieć możliwość podzielenia zadania
obliczeniowego na niezależne części.
Mój przykład
W moim przykładzie używam czterech wątków do obliczenia silni. Obliczanie silni w ten sposób nie ma
większego sensu i służy tu prostemu zobrazowaniu, na czym rzecz polega.
Omówię to na przykładzie silni z 12.
Silnia z N to iloczyn liczb od 1 do N. (Silnia z 0 jest 1.)
Dla 12 jest to 1 * 2 * 3 * 4 * 5 * 6 * 7 * 8 * 10 * 11 * 12.
Łatwo zauważyć, że taki iloczyn można podzielić na iloczyny częściowe np.:
(1 * 2 * 3) * (4 * 5 * 6) * (7 * 8 * 9) * (10 * 11 * 12).
Celowo utworzyłem 4 iloczyny częściowe, oddzielone nawiasami, bo mam zamiar przydzielić każdy z nich
jednemu z czterech wątków, którymi się posługuję w przykładzie.
Na koniec pomnożę iloczyny częściowe przez siebie i otrzymam wynik.
Synchronizacja różnych wątków
Warto zauważyć, że to końcowe mnożenie zostanie również wykonane przy użyciu wątków.
Ponieważ nie wiadomo, w jakim momencie poszczególne wątki wyliczą iloczyny częściowe, należy
wprowadzić synchronizację wątków. Wygląda to tak:
Pierwszy wątek pobiera własny, już obliczony iloczyn częściowy i ustawia flagę gotowości tego
iloczynu do pobrania przez inny wątek.
Drugi wątek po obliczeniu własnego iloczynu czeka na pojawienie się flagi gotowości pierwszego wątku
i mnoży własny iloczyn przez iloczyn z pierwszego wątku, po czym również ustawia flagę gotowości do
pobrania iloczynu przez trzeci wątek. Itd., co można zapisać jako:
Wątek 1: iloczyn_końcowy_1 = iloczyn_częściowy_1
Wątek 2: iloczyn_końcowy_2 = iloczyn_końcowy_1 * iloczyn_częściowy_2
Wątek 3: iloczyn_końcowy_3 = iloczyn_końcowy_2 * iloczyn_częściowy_3
Wątek 4: iloczyn_końcowy_4 = iloczyn_końcowy_3 * iloczyn_częściowy_4
Dzięki synchronizacji przez flagi odbędzie się to w takiej, a nie innej kolejności.
Wynikiem obliczeń jest iloczyn_końcowy_4.
Losowość
Abyś mógł się przekonać, że kolejność zadań wykonywanych przez wątki jest losowa (za wyjątkiem
przypadku synchronizacji) dodałem przycisk ‘Kolejność’.
Praktycznie za każdym razem jest inna.
Przypadek ogólny
Dla liczb, które nie dzielą się przez 4 (ilość wątków) np. 13, ostatni iloczyn jest rozszerzany.
Dla 13 będzie on wyglądał (10 * 11 * 12 * 13). Jest to prosta metoda, ale można się pokusić o
przerobienie programu tak, aby np. dla 14 utworzył iloczyny:
(1 * 2 * 3 * 13) * (4 * 5 * 6 * 14) * (7 * 8 * 9) * (10 * 11 * 12),
aby możliwie równomiernie obciążyć wątki.
Dla liczb zbyt małych, aby „obsadzić” wszystkie wątki, należy przyjąć iloczyny częściowe równe 1.
(Gdybyśmy dodawali wyrazy a nie mnożyli, byłoby to 0).
Uruchomienie programu
Aby uruchomić program, należy utworzyć nowy projekt z Form1, Unit1 i wkleić całą zawartość mojego
Unit1 oraz powiązać w zdarzenia OnCreate i OnClose w kodzie z formą.
Wszystkie kontrolki zostaną utworzone automatycznie po uruchomieniu.
Wykonanie obliczeń
W żółte pole należy wpisać liczbę od 0 do 20, której silnię chcemy obliczyć.
Wynik po kliknięciu Oblicz pojawi się w zielonym polu.
unit Unit1; interface uses Windows, SysUtils, Classes, Forms, Graphics, StdCtrls, ExtCtrls, Controls; const ThreadCount = 4; //ilość wątków JobCount = 4; //ilość zadań w wątku type TThreadCalc = class(TThread) private Tag: integer; //numer wątku Job: integer; //numer zadania Succ: array [0..JobCount - 1] of integer; //kolejność wykonywania zadań //przez wszystkie wątki FGetSucc: boolean; //jeśli true, badana jest kolejność protected procedure Execute; override; procedure GetData; //pobranie danej do obliczenia silni procedure PutData; //wyświtlenie rezultatu procedure CalcProc; //procedura obliczeniowa procedure GetSucc; //bada kolejność public constructor CreateWithPar(aTag: integer; aFGetSucc: boolean = false); //konstruktor z parametrami end; type TForm1 = class(TForm) procedure FormCreate(Sender: TObject); procedure Calc(Sender: TObject); procedure TimerTimer(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); public Threads: array [0..ThreadCount - 1] of TThreadCalc; //tablica wątków Edits : array [0..JobCount - 1, 0..ThreadCount - 1] of TEdit; //edity do wyświetlenia wyników pośrednich //lub kolejności Timer: TTimer; //warto umieścić na wypadek niepowodzeń procedure ClearEdits(Sender: TObject); procedure RunThreads(aFGetSucc: boolean = false); //uruchamia wszystkie wątki end; type TValue = record IntValue: int64; StrValue: string; IsInt : boolean; //flaga gotowości wartości int64 do dalszych obliczeń IsStr : boolean; //flaga gotowości wartości string do dalszych obliczeń end; var Form1: TForm1; Value: array [0..JobCount - 1, 0..ThreadCount - 1] of TValue; //tablica wyników pośrednich //służy także do synchronizacji obliczeń SuccCtr: integer; //licznik do badania kolejności wykonywania zadań //przez wszystkie wątki implementation {$R *.dfm} constructor TThreadCalc.CreateWithPar(aTag: integer; aFGetSucc: boolean = false); //tworzy wątek //jeśli aFGetSucc = true zbiera informacje o kolejności var iJob: integer; begin Tag := aTag; FGetSucc := aFGetSucc; for iJob := 0 to JobCount - 1 do //ustawienie flag wartości //na brak gotowości do dalszych obliczeń begin Value[iJob, Tag].IsInt := false; Value[iJob, Tag].IsStr := false; end; Self := Create(false); end; procedure TThreadCalc.GetData; //pobranie danej do obliczenia silni begin Value[Job, Tag].IntValue := StrToInt(Form1.Edits[Job, 0].Text); Value[Job, Tag].IsInt := true; end; procedure TThreadCalc.PutData; //wyświetlenie wyników pośrednich //lub kolejności var iJob: integer; begin if not FGetSucc then begin for iJob := 1 to JobCount - 1 do if Value[iJob, Tag].IsInt then Form1.Edits[iJob, Tag].Text := IntToStr(Value[iJob, Tag].IntValue) else if Value[iJob, Tag].IsStr then Form1.Edits[iJob, Tag].Text := Value[iJob, Tag].StrValue else Form1.Edits[iJob, Tag].Text := ''; end else for iJob := 0 to JobCount - 1 do Form1.Edits[iJob, Tag].Text := IntToStr(Succ[iJob]); if Tag = 0 then Form1.Edits[0, Tag].ReadOnly := false; end; procedure TThreadCalc.CalcProc; //procedura obliczeniowa procedure Job0; begin Job := 0; Synchronize(GetData); //używamy Synchronize po pobieramy daną z Form1 //tj. wątku głównego GetSucc; end; procedure Job1; //zadanie przygotowuje iloczyn częściowy //np. dla 12! i Tag = 1 jest to 4 * 5 * 6 var s: string; v: int64; i: integer; Max, Min: int64; begin Job := 1; v := Value[Job - 1, Tag].IntValue; Min := Tag + Tag * (v div (ThreadCount + 1)) + 1; Max := Tag + (Tag + 1) * (v div (ThreadCount + 1)) + 1; if Tag = ThreadCount - 1 then Max := v; if Max > v then Max := v; for i := Min to Max do if i = Min then s := IntToStr(i) else s := s + '*' + IntToStr(i); if s = '' then s := '1'; Value[Job, Tag].StrValue := s; Value[Job, Tag].IsStr := true; GetSucc; end; procedure Job2; //zadanie wylicza wartość iloczynu częściowego var v: int64; s: string; begin Job := 2; s := Value[Job - 1, Tag].StrValue; v := 1; s := s + '*'; while Pos('*', s) > 0 do begin v := v * StrToInt(Copy(s, 1, Pos('*', s) - 1)); s := Copy(s, Pos('*', s) + 1, Length(s)); end; Value[Job, Tag].IntValue := v; Value[Job, Tag].IsInt := true; GetSucc; end; procedure Job3; //zadanie mnoży iloczyny częściowe //np. dla Tag = 1 jest to //iloczyn_częściowy(Tag = 0) * iloczyn_częściowy(Tag = 1) //w zadaniu występuje synchronizacja wątków begin Job := 3; if Tag = 0 then begin Value[Job, Tag].IntValue := Value[Job - 1, Tag].IntValue; Value[Job, Tag].IsInt := true; end else begin while not Value[Job, Tag - 1].IsInt do //synchronizacja wątków ; //tu: oczekiwanie na zakończenie //obliczeń przez wątek (Tag - 1) Value[Job, Tag].IntValue := Value[Job - 1, Tag].IntValue * Value[Job, Tag - 1].IntValue; Value[Job, Tag].IsInt := true; end; GetSucc; Synchronize(PutData); //używamy Synchronize bo wpisujemy dane do Form1 //tj. wątku głównego Terminate; //jest to ostatnie zadanie i kończy wątek end; begin Job0; Job1; Job2; Job3; end; procedure TThreadCalc.GetSucc; begin if not FGetSucc then Exit; Inc(SuccCtr); Succ[Job] := SuccCtr; end; procedure TThreadCalc.Execute; begin FreeOnTerminate := true; Priority := tpNormal; CalcProc; end; procedure TForm1.Calc(Sender: TObject); //uruchamia obliczenia begin try if StrToInt(Edits[0, 0].Text) in [0..20] then begin Edits[0, 0].ReadOnly := true; if (Sender as TButton).Tag = 0 then RunThreads else if (Sender as TButton).Tag = 1 then RunThreads(true); end; except end; end; procedure TForm1.RunThreads(aFGetSucc: boolean = false); //uruchamia wszystkie wątki var iThread: integer; begin ClearEdits(nil); SuccCtr := 0; for iThread := 0 to ThreadCount - 1 do Threads[iThread] := TThreadCalc.CreateWithPar(iThread, aFGetSucc); end; procedure TForm1.TimerTimer(Sender: TObject); begin Application.ProcessMessages; end; procedure TForm1.ClearEdits(Sender: TObject); //czyści edity var iJob, iThread: integer; begin for iJob := 0 to JobCount - 1 do for iThread := 0 to ThreadCount - 1 do if not ((iJob = 0) and (iThread = 0)) then Edits[iJob, iThread].Text := ''; end; procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); //należy powiązać z OnClose w ObjectInspectorze var iThread: integer; begin for iThread := 0 to ThreadCount - 1 do try Threads[iThread].Terminate; except end; end; procedure TForm1.FormCreate(Sender: TObject); //tworzy wszystko co jest potrzebne do sprawdzenia jak to działa //należy powiązać z OnCreate w ObjectInspectorze const constWidth = 240; constHeight = 21; var iJob, iThread: integer; l: TLabel; b: TButton; begin Width := 1000; Height := 600; Position := poScreenCenter; Font.Name := 'Tahoma'; Font.Size := 8; Caption := 'Obliczanie silni na 4 wątkach, podaj wartość od 0 do 20'; for iJob := 0 to JobCount - 1 do begin l := TLabel.Create(Self); l.Left := 10 + iJob * constWidth; l.Top := 14; case iJob of 0 : l.Caption := 'Zadanie ' + IntToStr(iJob) + ', wejście'; 1, 2: l.Caption := 'Zadanie ' + IntToStr(iJob); 3 : l.Caption := 'Zadanie ' + IntToStr(iJob) + ', wyjście'; end; InsertControl(l); for iThread := 0 to ThreadCount - 1 do begin Edits[iJob, iThread] := TEdit.Create(Self); Edits[iJob, iThread].Width := constWidth; Edits[iJob, iThread].Left := 10 + iJob * constWidth; Edits[iJob, iThread].Top := 30 + iThread * constHeight; if not ((iJob = 0) and (iThread = 0)) then Edits[iJob, iThread].ReadOnly := true; InsertControl(Edits[iJob, iThread]); end; end; Edits[0, 0].OnChange := ClearEdits; Edits[0, 0].Color := clYellow; Edits[JobCount - 1, ThreadCount - 1].Color := clLime; b := TButton.Create(Self); b.Left := Edits[0, 0].Left; b.Top := 30 + ThreadCount * constHeight; b.Width := constWidth; b.Tag := 0; b.Caption := 'Oblicz'; b.OnClick := Calc; InsertControl(b); b := TButton.Create(Self); b.Left := Edits[1, 0].Left; b.Top := 30 + ThreadCount * constHeight; b.Width := constWidth; b.Tag := 1; b.Caption := 'Kolejność'; b.OnClick := Calc; InsertControl(b); Timer := TTimer.Create(Self); Timer.Interval := 100; Timer.OnTimer := TimerTimer; Timer.Enabled := true; end; end.
4 komentarze
Używam Synchronize ponieważ modyfikuję zawartość editów na formie głównej, a w takiej sytuacji (akurat na przykładzie UpdateCaption) Borland zaleca takie postępowanie. Wystarczy wyklikać File/New/...ThreadObject i samo się to wpisuje do unitu. Chciałem w tych editach pokazać obliczenia cząskowe, których nikt by nie wypisywał w postaci stringa np. '1 * 2 * 3', gdyby chodziło tylko o szybkość obliczeń, a tu chodzi także o prezentację podziału na obliczenia cząstkowe.
Jeśli chodzi o Sleep(1ms) to jest to czas strasznie długi w porównaniu do większości obliczeń.
Tak czy inaczej trzeba by dobrać odpowiednią proporcję między czasem trwania zadań (Job0, Job1,...), wynikającym z ich złożoności, a czasem trwania Sleep, żeby maksymalnie wykorzystać procesor. Można jeszcze skorzystać z GetTickCount.
Trochę się boję prowadzić dalsze testy, skoro Azarien mnie ostrzega o poważnym problemie jaki mam z chłodzeniem procesora. Niby w Bios-ie mam ustawione zabezpieczenie przed przegrzaniem procesora i do tej pory zwalniał pracę, ale rzeczywiście nigdy nie było pisku. Muszę to sprawdzić.
Ten cały artykuł powstał na bazie eksperymentu. Zanim przeczytałem wasze komentarze miałem zamiar dopisać, że ten eksperyment może być niebezpieczny dla procesora - jak się okazuje dla mojego.
Jeżeli przy obciążeniu 100% zaczyna ci piszczeć, to masz poważny problem z chłodzeniem procesora.
Jaki sens ma dzielenie tego na wątki skoro i tak wszystko wykonujesz w kontekście wątku głównego przez Synchronize?
I zamiast Synchronize(ProcessMessages); można było dać Sleep(ilosc ms)
kod raczej nie zabija procesorów :) jeżeli temperatura ci rośnie tak że włącza się piszczek to znaczy że chłodzenie się nie wyrabia. Choćby dlatego że może radiator niedokładnie przylega do procesora, czy nawet dlatego że jest już mocno zakurzony. W normalnych warunkach temperatura nie powinna przekraczać 70 stopni (przynajmniej jak dla mnie), nieważne jakie obciążenie.