Sign in Join
Wiki copytemporary

Example of multi-threaded application: array of threads/pl: what changed

Revision 2 to 3. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11Przykład aplikacji wielowątkowej: tablica wątków
22
33Tutaj chcę pokazać przykład, jak utworzyć wiele wątków i poczekać, aż zakończą swoje zadania (nie potrzebuję żadnej synchronizacji). Piszę ten tutorial, ponieważ po przeczytaniu \[\[Multithreaded Application Tutorial/pl\|Samouczek aplikacji wielowątkowej\] nie było dla mnie oczywiste napisanie takiego programu. Pisałem aplikację dla systemu macOS, ale powstały kod powinien działać na każdym systemie.
44
55Załóżmy, że mamy następującą pętlę:
66
77``` pascal
88...
99for i:=1 to n do begin
1010 power(i,0.5)
1111end;
1212...
1313```
1414
1515Ta pętla wykonuje n razy potęgowanie iteratora ze stopniem 0,5 (co odpowiada pierwiastkowaniu 2 stopnia). Użyjmy wątków, aby osiągnąć to samo zadanie równolegle.
1616
1717## Czy potrzebujemy wielu wątków?
1818
1919W nowoczesnych komputerach z wielordzeniowymi procesorowymi, użycie wielu wątków może radykalnie zwiększyć wydajność. Jednak nowoczesne komputery są bardzo szybkie, a kod wielowątkowy jest często trudniejszy do debugowania i konserwacji niż kod jednowątkowy. Należy rozważyć, czy zaoszczędzony czas przetwarzania uzasadnia złożoność programowania wielowątkowego i czy algorytm dobrze nadaje się do obliczeń równoległych. Na koniec należy zauważyć, że niektóre algorytmy, które mogą czerpać korzyści z obliczeń równoległych, mogą skorzystać na wykorzystaniu wątków procesora (jak pokazano tutaj), podczas gdy inne mogą być bardziej dostosowane do procesora graficznego (gdzie narzędzia takie jak OpenCL mogą być bardziej optymalne).
2020
2121## Zarządzanie pamięcią
2222
2323Ponieważ wiele wątków będzie działać jednocześnie, należy upewnić się, że nie występują w nich problemy z rywalizacją o pamięć. Jeśli wiele wątków zapisuje dane w tych samych lokalizacjach w pamięci, wystąpią problemy z rywalizacją o pamięć. Niektóre algorytmy nie nadają się do wielowątkowości, ponieważ każde obliczenie zależy od wcześniejszych wyników. Z drugiej strony wielowątkowość działa bardzo efektywnie w przypadku problemów, w których obliczenia można wykonywać niezależnie i równolegle. W tym przykładzie rozwiążemy problem, który jest całkowicie niezależny i dlatego łatwy do zrealizowania wieloma wątkami. Zaawansowane algorytmy będą musiały korzystać z funkcji blokowania pamięci, aby uniknąć rywalizacji.
2424
2525W naszym przykładzie każdy wątek będzie zapisywał w różnych lokalizacjach pamięci. W szczególności utworzymy tablicę 1..n i obliczymy wartość power(i,0.5) (potegi), gdzie i należy do zakresu 1..n. Każdy wątek otrzyma niezależną część zakresu do obliczenia. Rozważ n=1000. Jeśli użyjemy jednego wątku, będzie on miał do wykonania zadanie z całego zakresu 1..1000, natomiast jeśli zastosujemy dwa wątki, jeden zajmie się zakresem 1..500, a drugi 501..1000. W ten sposób wątki będą pracować nad wypełnieniem różnych części naszej tablicy pamięci.
2626
2727## 1. Wykryj liczbę dostępnych rdzeni.
2828
2929Komputer posiadający tylko jeden rdzeń nie odniesie korzyści z obsługi wątków, podczas gdy komputer z czterema fizycznymi rdzeniami, każdy z funkcją hyperthreading (będący w stanie wykonywać dwa zadania jednocześnie), będzie w stanie przetwarzać do ośmiu zadań jednocześnie. Niżej pokazany moduł „cpucount” podaje liczbę dostępnych rdzeni. Możesz użyć tej opcji, aby określić, z ilu wątków powinien korzystać Twój program na danym komputerze. W przypadku komputerów z czterema lub więcej rdzeniami wiele osób chce uruchamiać wątki n-1 (gdzie n to liczba rdzeni), rezerwując jeden rdzeń na potrzeby interfejsu graficznego i innych zadań, ponieważ różnica w wydajności między wątkami n i n-1 nie będzie istotna z tak dużą liczbą rdzeni.
3030
3131> **Note:** Definicja funkcji użyta poniżej dla FPsysctl() dotyczy FPC v3.0.4 - Dla FPC v3.2.0 i v3.3.1 (trunk) pierwszym argumentem nie jest już *pchar*, ale *pcint*. Dostosuj odpowiednio przykładowy kod.
3232
3333``` pascal
3434unit cpucount;
3535
3636{$mode objfpc}{$H+}
3737
3838interface
3939//zwraca liczbę rdzeni: komputer z dwoma rdzeniami hiperwątkowymi zgłosi 4
4040function GetLogicalCpuCount: Integer;
4141
4242implementation
4343
4444{$IF defined(windows)}
4545uses windows;
4646{$endif}
4747
4848{$IF defined(darwin)}
4949uses ctypes, sysctl;
5050{$endif}
5151
5252{$IFDEF Linux}
5353uses ctypes;
5454
5555const _SC_NPROCESSORS_ONLN = 83;
5656function sysconf(i: cint): clong; cdecl; external name 'sysconf';
5757{$ENDIF}
5858
5959function GetLogicalCpuCount: integer;
6060// zwraca dobrą domyślną liczbę wątków w tym systemie
6161{$IF defined(windows)}
6262//zwraca całkowitą liczbę procesorów dostępnych w systemie, w tym logiczne procesory hiperwątkowe
6363var
6464 i: Integer;
6565 ProcessAffinityMask, SystemAffinityMask: DWORD_PTR;
6666 Mask: DWORD;
6767 SystemInfo: SYSTEM_INFO;
6868begin
6969 if GetProcessAffinityMask(GetCurrentProcess, ProcessAffinityMask, SystemAffinityMask)
7070 then begin
7171 Result := 0;
7272 for i := 0 to 31 do begin
7373 Mask := DWord(1) shl i;
7474 if (ProcessAffinityMask and Mask)<>0 then
7575 inc(Result);
7676 end;
7777 end else begin
7878 //nie można uzyskać maski koligacji, więc po prostu raportujemy całkowitą liczbę procesorów
7979 GetSystemInfo(SystemInfo);
8080 Result := SystemInfo.dwNumberOfProcessors;
8181 end;
8282end;
8383{$ELSEIF defined(UNTESTEDsolaris)}
8484 begin
8585 t = sysconf(_SC_NPROC_ONLN);
8686 end;
8787{$ELSEIF defined(freebsd) or defined(darwin)}
8888var
8989 mib: array[0..1] of cint;
9090 len: cint;
9191 status: integer;
9292begin
9393 mib[0] := CTL_HW;
9494 mib[1] := HW_NCPU;
9595 len := sizeof(Result);
9696 status := fpsysctl(pchar(@mib), Length(mib), @Result, @len, Nil, 0);
9797 if status <> 0 then WriteLn('Error in fpsysctl()');
9898end;
9999{$ELSEIF defined(linux)}
100100 begin
101101 Result:=sysconf(_SC_NPROCESSORS_ONLN);
102102 end;
103103
104104{$ELSE}
105105 begin
106106 Result:=1;
107107 end;
108108{$ENDIF}
109109end.
110110```
111111
112112## 2. Utwórz niestandardową klasę wątków.
113113
114114Do definiowania nowych wątków używam osobnego modułu. Zauważ, że ustawiam „FreeOnTerminate” na false - więc mój program będzie musiał pozbyć się każdego wątku, gdy się zakończy. Ułatwia to żonglowanie wieloma wątkami (jeśli ustawisz FreeOnTerminate na true i uruchomisz wiele bardzo szybkich zadań, możliwe jest, że wątek zostanie zwolniony, zanim program sprawdzi, czy wątek jest ukończony – a sprawdzenie nieistniejącego wątku może spowodować wyjątek). Ustawiając FreeOnTerminate na false, mogę mieć pewność, że każdy wątek zakończył się pomyślnie.
115115
116116Dobrze zachowujący się wątek w pętli powinien regularnie sprawdzać zakończenie, aby w przypadku, gdy procesy zewnętrzne (np. Destroy) chciały przerwać wykonywanie, nie musiały czekać wiecznie. Oryginalny kod tego nie robił, więc nie można było ukończyć zwolnienia pamięci (free), dopóki wszystkie obliczenia nie zostały zakończone.
117117
118118Kontrola podstawowego kodu pokazuje, że po wyjściu z polecenia „Execute” wywoływana jest funkcja OnTerminate, ale opcja Terminated nie jest ustawiona. Ponieważ w przykładzie nie używa się już funkcji waitfor, ale po prostu sprawdza się Terminated, to dodałem Terminate po zakończeniu pętli.
119119
120120``` pascal
121121unit mythreads;
122122{$mode objfpc}{$H+}
123123interface
124124uses
125125 Classes, SysUtils, Math;
126126type
127127 TData = array of double;
128128 PData = ^TData;
129129 Type
130130 TMyThread = class(TThread)
131131 private
132132 protected
133133 tPtr: PData;
134134 tstart,tfinish: integer;
135135 procedure Execute; override;
136136 public
137137 property Terminated;
138138 Constructor Create(lstart, lfinish: integer; var lPtr: PData);
139139 end;
140140
141141implementation
142142
143143 constructor TMyThread.Create(lstart, lfinish: integer; var lPtr: PData);
144144 begin
145145 FreeOnTerminate := False;
146146 tstart := lstart;
147147 tfinish := lfinish;
148148 tPtr := lPtr;
149149 inherited Create(false);
150150 end;
151151 procedure TMyThread.Execute;
152152 var
153153 i: integer;
154154 begin
155155 i:= tstart;
156156 While not Terminated and (i<= tfinish) do
157157 //Dobrze zachowujący się wątek w pętli powinien regularnie sprawdzać zakończenie
158158 begin
159159 //for i := tstart to tfinish do
160160 tPtr^[i] := power(i,0.5);
161161 inc(i);
162162 end;
163163 Terminate;
164164 end;
165165
166166end.
167167```
168168
169169## 3. Tworzenie głównego programu.
170170
171171Jeśli korzystasz z systemu Linux/Unix, musisz dodać moduł 'cthreads' do modułu głównego 'main', a nie do modułu z wątkami! Obecnie Lazarus domyślnie dodaje moduły warunkowo:
172172
173-` {$IFDEF UNIX}`\
174-` cthreads,`\
175-` {$ENDIF}`\
176-` {$IFDEF HASAMIGA}`\
177-` athreads,`\
178-` {$ENDIF}`
173+```text
174+ {$IFDEF UNIX}
175+ cthreads,
176+ {$ENDIF}
177+ {$IFDEF HASAMIGA}
178+ athreads,
179+ {$ENDIF}
180+```
179181
180182więc w takiej sytuacji nie musisz tego już robić. W przypadku MS Windows nie jest to potrzebne.
181183
182184Należy pamiętać, że istnieją dwa sposoby sprawdzenia, czy wszystkie wątki zostały zakończone. Możesz użyć wbudowanej funkcji „waitFor” - działa to bardzo dobrze, ale na moim komputerze Mac zauważyłem, że odświeża się tylko co 100 ms. Jest to idealne rozwiązanie dla rzeczywistych programów (używamy wątków tylko w przypadku problemów obliczeniowo wolnych) i zmniejsza obciążenie wątku. Jednak w przypadku szybkich przykładowych testów porównawczych może ukryć zalety wielowątkowości (ponieważ operacje wymagają minimum 100 ms niezależnie od liczby wątków). Dlatego w tym przykładzie wykrywam status zakończenia wątku co 2 ms. Zapewnia to dokładniejszy czas analizy porównawczej.
183185
184186Pamiętaj, aby zwolnić każdy wątek, gdy już z nim skończysz. Ponieważ ustawiliśmy „FreeOnTerminate := False”, program musi to zrobić jawnie.
185187
186188Wskazówka: w moim środowisku IDE Lazarusa nie mogłem debugować aplikacji wielowątkowych, jeśli nie korzystałem z 'pthreads'. Czytałem, że jeśli użyjesz 'cmem', program będzie działał szybciej, ale zdecydowanie zalecam sprawdzenie tego pod kątem konkretnego przypadku (mój program zawiesza się, gdy używam 'cmem').
187189
188190``` pascal
189191uses // cmem,pthreads,
190192 cthreads, Classes, SysUtils, CustApp, MyThreads, Math, cpucount;
191193
192194procedure DoUnThreaded (nValues: integer);
193195var
194196 dataArray: TData;
195197 i: integer;
196198 StartMS: double;
197199begin
198200 if (nValues < 1) then exit;
199201 StartMS:=timestamptomsecs(datetimetotimestamp(now));
200202 setlength(dataArray, nValues+1);//+1 since indexed 0..n-1
201203 for i:=1 to nValues do
202204 dataArray[i] := power(i,0.5); ;
203205 Writeln('Przetworzono szeregowo '+inttostr(nValues)+' wartości w czasie '+floattostr(timestamptomsecs(datetimetotimestamp(now))-StartMS)+'ms, wykonując '+inttostr(nValues)+'^0.5 = '+floattostr(dataArray[nValues]));
204206end;
205207
206208procedure DoThreading (nThreadsIn, nValues: integer);
207209var
208210 threadArray: array of TMyThread;
209211 dataArray: TData;
210212 lData : PData;
211213 nThreads, i,lStart,lFinish: integer;
212214 StartMS: double;
213215begin
214216 if (nThreadsIn < 1) or (nValues < 1) then exit;
215217 nThreads := nThreadsIn;
216218 if nThreads > nValues then nThreads := nValues;
217219 StartMS:=timestamptomsecs(datetimetotimestamp(now));
218220 setlength(threadArray,nThreads+1);//+1 do indeksowania 0..n-1
219221 setlength(dataArray, nValues+1);//+1 do indeksowania 0..n-1
220222 lData := @dataArray;
221223 lStart := 1;
222224 for i:=1 to nThreads do begin
223225 if i < nThreads then
224226 lFinish:=i*(nValues div nThreads)
225227 else
226228 lFinish:= nValues;
227229 threadArray[i]:= TMyThread.Create(lStart, lFinish, lData);
228230 //Writeln('Przetwarzanie '+inttostr(i)+' wątkowe zakresu '+inttostr(lStart)+'..'+inttostr(lFinish));
229231 lStart := lFinish+1;
230232 end;
231233 //for i:=1 to nThreads do if not ThreadArray[i].Terminated then Sleep(2); //Logicznie źle, bo wprowadza opóźnienie n * 2ms
232234 //for i:=1 to nThreads do threadArray[i].waitFor; //wygląda na to, że ma macOS usypia się na 100ms
233235
234236 for i:=1 to nThreads do
235237 While not ThreadArray[i].Terminated do Sleep(2); //nie kontynuuj, dopóki wszystkie wątki nie zostaną zakończone
236238
237239 for i:=1 to nThreads do threadArray[i].Free; //Free nie zostanie ukończone do czasu zakończenia procesu wykonywania wątku.
238240 Writeln('Przetworzono '+inttostr(nThreads)+' wątkowo '+inttostr(nValues)+' wartości w czasie '+floattostr(timestamptomsecs(datetimetotimestamp(now))-StartMS)+'ms, wykonując '+inttostr(nValues)+'^0.5 = '+floattostr(dataArray[nValues]));
239241end;
240242
241243begin
242244 Writeln('Komputer zgłasza '+inttostr(GetLogicalCpuCount)+' rdzeni: to prawdopodobnie jest optymalna liczba wątków');
243245 DoUnthreaded(10);
244246 DoThreading(1,10);
245247 DoThreading(2,10);
246248 DoThreading(4,10);
247249 DoThreading(8,10);
248250 DoUnthreaded(100000000);
249251 DoThreading(1,100000000);
250252 DoThreading(2,100000000);
251253 DoThreading(4,100000000);
252254 DoThreading(8,100000000);
253255end.
254256```
255257
256258## Wyniki.
257259
258260Wyniki pokazują, że występuje opóźnienie w tworzeniu wątków, ale w przypadku dużych zadań wiele równoległych wątków przewyższa to przetwarzanie szeregowe. Należy pamiętać, że gdy liczba wątków przekracza liczbę rdzeni (4 w tym komputerze), dodatkowe wątki nie przynoszą żadnych korzyści. Nie należy oczekiwać, że prędkość będzie idealnie skalować się wraz z liczbą wątków: wielowątkowość wiąże się z pewnym obciążeniem, a większość nowoczesnych procesorów będzie działać nieco szybciej, gdy wykonywane jest tylko jedno intensywne zadanie, niż podczas wykonywania uruchomionych zadań obciążających wiele rdzeni ('turboboost').
259261
260262- Komputer zgłasza 4 rdzeni: to prawdopodobnie jest optymalna liczba wątków
261263- Przetworzono szeregowo 10 wartości w czasie 0ms, wykonując 10^0.5 = 3.16227766016838
262264- Przetworzono 1 wątkowo 10 wartości w czasie 3ms, wykonując 10^0.5 = 3.16227766016838
263265- Przetworzono 2 wątkowo 10 wartości w czasie 5ms, wykonując 10^0.5 = 3.16227766016838
264266- Przetworzono 4 wątkowo 10 wartości w czasie 9ms, wykonując 10^0.5 = 3.16227766016838
265267- Przetworzono 8 wątkowo 10 wartości w czasie 19ms, wykonując 10^0.5 = 3.16227766016838
266268- Przetworzono szeregowo 100000000 wartości w czasie 10214ms, wykonując 100000000^0.5 = 10000
267269- Przetworzono 1 wątkowo 100000000 wartości w czasie 10320ms, wykonując 100000000^0.5 = 10000
268270- Przetworzono 2 wątkowo 100000000 wartości w czasie 5894ms, wykonując 100000000^0.5 = 10000
269271- Przetworzono 4 wątkowo 100000000 wartości w czasie 3801ms, wykonując 100000000^0.5 = 10000
270272- Przetworzono 8 wątkowo 100000000 wartości w czasie 3733ms, wykonując 100000000^0.5 = 10000
271273
272274---
273275*From the Free Pascal wiki, page "Example of multi-threaded application: array of threads/pl", as it stood on 2023-09-23 after a change by Slawek, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*
274276Category: Multitasking
275277Category: Parallel programming
276278Category: Tutorials
277279Category: macOS

Back to history