Sign in Join
Wiki copytemporary

Multithreaded Application Tutorial/pl: what changed

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

OldNewLine
1-<div style="font-size:1.88em;margin:0.75em 0;">
2-
31Kurs tworzenia aplikacji wielowątkowych
4-
5-</div>
62
73## Wprowadzenie
84
95Na tej stronie spróbujemy wyjaśnić, w jaki sposób napisać i debugować aplikacje wielowątkowe przy pomocy Free Pascala i Lazarusa. Aplikacja wielowątkowa to program, który tworzy dwa lub więcej wątków wykonawczych działających w tym samym czasie. Jeśli nie miałeś dotąd do czynienia z wielowątkowością, przeczytaj akapit „Czy potrzebujesz wielowątkowości?” aby ustalić, czy jest ci to naprawdę potrzebne, bo być może zaoszczędzisz sobie bólu głowy.
106
117Pierwszy wątek nazywany jest głównym wątkiem. Główny wątek to ten, który jest tworzony przez System Operacyjny, to ten sam, w którym nasza aplikacja rozpoczyna działanie. Główny wątek **musi być** jedynym wątkiem, który aktualizuje komponenty do komunikacji z użytkownikiem: w przeciwnym wypadku, aplikacja może się zawiesić.
128
139Podstawowym założeniem jest to, aby aplikacja mogła przetwarzać pewne dane w tle, tj. w drugim wątku, podczas gdy użytkownik może kontynuować pracę przy użyciu głównego wątku.
1410
1511Innym zastosowaniem wątków jest po prostu możliwość lepszej reakcji programu. Jeśli tworzysz dużą aplikację lub gdy użytkownik naciśnie przycisk aplikacji rozpoczynając jakiś duży proces ... dopóki trwa przetwarzanie, ekran przestaje odpowiadać, co powoduje błędne lub mylące wrażenie, że aplikacja jest zawieszona. Jeśli duży proces przebiega w drugim wątku, aplikacja zachowuje się (prawie) tak, jakby była w stanie bezczynności. W tym przypadku dobrym pomysłem jest aby, przed rozpoczęciem wątku, wyłączyć odpowiednie przyciski na formularzu, w celu uniknięcia ponownego uruchomienia drugiego wątku przez użytkownika.
1612
1713Jeszcze innym zastosowaniem wielowątkowości może być serwer, który jest w stanie dać odpowiedź wielu klientom, w tym samym czasie.
1814
1915## Czy potrzebujesz wielowątkowości?
2016
2117Jeśli dopiero poznajesz wielowątkowość i chciałbyś tylko stworzyć aplikację z szybszym czasem reakcji w chwili gdy wykonuje ona umiarkowanie długotrwałe zadanie, wówczas wielowątkowość może być nadmiarowa w stosunku do wymagań. Aplikacje wielowątkowe są zawsze trudniejsze do debugowania i często są znacznie bardziej skomplikowane, jednak w wielu przypadkach wcale nie potrzebujesz używać wielowątkowości. Jeden wątek jest wystarczający. Możesz podzielić czasochłonne zadanie na kilka mniejszych części, oraz użyć procedurę Application.ProcessMessages. Procedura ta pozwala bibliotece LCL obsłużyć wszystkie oczekujące komunikaty i powrócić do miejsca jej wywołania. Główną ideą jest to, aby wywoływać Application.ProcessMessages w regularnych odstępach czasu w trakcie wykonywania długotrwałego zadania, np. po to aby sprawdzić, czy użytkownik kliknął na jakąś kontrolkę lub czy wskaźnik postępu musi zostać przemalowany albo zdarzyło się jeszcze coś innego.
2218
2319Przykład: Czytanie dużego pliku i jego przetwarzanie. Zobacz: examples/multithreading/singlethreadingexample1.lpi.
2420
2521Wielowątkowość jest potrzebna tylko w przypadku
2622
2723- używania uchwytów blokujących, takich jak w komunikacji sieci
2824- korzystania z wielu procesorów jednocześnie (SMP)
2925- algorytmów i bibliotek, które muszą być wywoływane przez API i jako takie nie mogą być podzielone na mniejsze części.
3026
3127Jeśli chcesz użyć wielowątkowości, aby zwiększyć prędkość przy użyciu wielu procesorów jednocześnie, sprawdź, czy twój obecny program w tej chwili wykorzystuje 100% zasobów 1 rdzenia CPU (na przykład, program może aktywnie korzystać z operacji wejścia-wyjścia, jak zapis do pliku i to zajmuje dużo czasu, ale nie obciąża procesora, i w tym przypadku program nie będzie działał szybciej z wieloma wątkami). Należy również sprawdzić, czy poziom optymalizacji jest ustawiony na maksymalny (3). Przełączając poziom optymalizacji z 1 na 3, program może stać się około 5 razy szybszy.
3228
3329## Moduły potrzebne do tworzenia aplikacji wielowątkowej
3430
3531Nie potrzebujesz żadnych specjalnych modułów do tego, aby pracować z systemem Windows. Jednak w przypadku Linuksa, Mac OS X i FreeBSD, należy użyć modułu cthreads i *musi* być on użyty jako pierwszy moduł projektu (program źródłowy, zwykle znajdujący się w pliku .lpr)!
3632
3733Dlatego twój kod aplikacji Lazarusa powinien wyglądać tak:
3834
3935``` pascal
4036program MyMultiThreadedProgram;
4137{$mode objfpc}{$H+}
4238uses
4339{$ifdef unix}
4440 cthreads,
4541 cmem, // menedżer pamięci c jest w niektórych systemach znacznie szybszy dla aplikacji wielowątkowych
4642{$endif}
4743 Interfaces, // to zawiera widżety LCL
4844 Forms
4945 { tu możesz dodać następne moduły },
5046```
5147
5248Jeśli o tym zapomnisz i użyjesz TThread, otrzymasz następujący błąd podczas uruchamiania:
5349
5450` This binary has no thread support compiled in.`\
5551` Recompile the application with a thread-driver in the program uses clause before other units using thread.`
5652
5753> **Note:** Jeśli pojawi się błąd linkera o tym, że nie znaleziono „mcount” to znaczy, że używasz jakiegoś modułu, który zawiera kod wielowątkowy i musisz dodać moduł cthreads lub użyć [[smart linking|sprytnego łączenia]].
5854
5955> **Note:** Jeśli w procedurze SYSTEM_NOTHREADERROR otrzymasz błąd: „Project raised exception class 'RunError(232)'” to znaczy, że twój kod potrzebuje wątków i musisz dodać moduł cthreads.
6056
6157## Przykład w czystym FPC
6258
6359Poniższy kod przedstawia bardzo prosty przykład. Testowany z FPC 3.0.4 na Win7.
6460
6561``` pascal
6662Program ThreadTest;
6763{ przetestuj możliwość aplikacji wielowątkowej }
6864{
6965 OUTPUT
7066wątek 1 uruchomiony
7167wątek 1 zmienna thri 0 Długość(s)= 1
7268wątek 1 zmienna thri 1 Długość(s)= 2
7369wątek 1 zmienna thri 2 Długość(s)= 3
7470wątek 1 zmienna thri 3 Długość(s)= 4
7571wątek 1 zmienna thri 4 Długość(s)= 5
7672wątek 1 zmienna thri 5 Długość(s)= 6
7773wątek 1 zmienna thri 6 Długość(s)= 7
7874wątek 1 zmienna thri 7 Długość(s)= 8
7975wątek 1 zmienna thri 8 Długość(s)= 9
8076wątek 1 zmienna thri 9 Długość(s)= 10
8177wątek 1 zmienna thri 10 Długość(s)= 11
8278wątek 1 zmienna thri 11 Długość(s)= 12
8379wątek 1 zmienna thri 12 Długość(s)= 13
8480wątek 1 zmienna thri 13 Długość(s)= 14
8581wątek 1 zmienna thri 14 Długość(s)= 15
8682wątek 2 uruchomiony
8783wątek 3 uruchomiony
8884wątek 1 zmienna thri 15 Długość(s)= 16
8985wątek 2 zmienna thri 0 Długość(s)= 1
9086wątek 3 zmienna thri 0 Długość(s)= 1
9187wątek 1 zmienna thri 16 Długość(s)= 17
9288...
9389...
9490wątek 5 zmienna thri 997 Długość(s)= 998
9591wątek 5 zmienna thri 998 Długość(s)= 999
9692wątek 5 zmienna thri 999 Długość(s)= 1000
9793wątek 5 zakończony
9894wątek 10 zmienna thri 828 Długość(s)= 829
9995wątek 9 zmienna thri 675 Długość(s)= 676
10096wątek 4 zmienna thri 656 Długość(s)= 657
10197wątek 10 zmienna thri 829 Długość(s)= 830
10298wątek 9 zmienna thri 676 Długość(s)= 677
10399wątek 9 zmienna thri 677 Długość(s)= 678
104100wątek 10 zmienna thri 830 Długość(s)= 831
105101wątek 10 zmienna thri 831 Długość(s)= 832
106102wątek 10 zmienna thri 832 Długość(s)= 833
107103wątek 10 zmienna thri 833 Długość(s)= 834
108104wątek 10 zmienna thri 834 Długość(s)= 835
109105wątek 10 zmienna thri 835 Długość(s)= 836
110106wątek 10 zmienna thri 836 Długość(s)= 837
111107wątek 10 zmienna thri 837 Długość(s)= 838
112108wątek 10 zmienna thri 838 Długość(s)= 839
113109wątek 10 zmienna thri 839 Długość(s)= 840
114110wątek 9 zmienna thri 678 Długość(s)= 679
115111...
116112...
117113wątek 4 zmienna thri 994 Długość(s)= 995
118114wątek 4 zmienna thri 995 Długość(s)= 996
119115wątek 4 zmienna thri 996 Długość(s)= 997
120116wątek 4 zmienna thri 997 Długość(s)= 998
121117wątek 4 zmienna thri 998 Długość(s)= 999
122118wątek 4 zmienna thri 999 Długość(s)= 1000
123119wątek 4 zakończony
12412010
125121
126122}
127123
128124uses
129125 {$ifdef unix}cthreads, {$endif} sysutils;
130126
131127const
132128 threadcount = 10;
133129 stringlen = 1000;
134130
135131var
136132 finished : longint;
137133
138134threadvar
139135 thri : ptrint;
140136
141137function f(p : pointer) : ptrint;
142138var
143139 s : ansistring;
144140begin
145141 Writeln('wątek ',longint(p),' uruchomiony');
146142 thri:=0;
147143 while (thri<stringlen) do begin
148144 s:=s+'1'; { stwórz opóźnienie }
149145 writeln('wątek ',longint(p),' zmienna thri ',thri,' Długość(s)= ',length(s));
150146 inc(thri);
151147 end;
152148 Writeln('wątek ',longint(p),' zakończony');
153149 InterLockedIncrement(finished);
154150 f:=0;
155151end;
156152
157153var
158154 i : longint;
159155
160156Begin
161157 finished:=0;
162158 for i:=1 to threadcount do
163159 BeginThread(@f,pointer(i));
164160 while finished<threadcount do ;
165161 Writeln(finished);
166162End.
167163```
168164
169165## Klasa TThread
170166
171167Poniższy przykład można znaleźć w katalogu examples/multithreading/.
172168
173169Aby utworzyć aplikację wielowątkową, najłatwiej jest użyć klasy TThread. Ta klasa pozwala w prosty sposób stworzyć dodatkowy wątek (obok głównego wątku). Zwykle wymagane jest przesłonięcie tylko dwóch metod: konstruktora Create i metody Execute.
174170
175171W konstruktorze przygotujesz wątek do uruchomienia. Ustawisz początkowe wartości dla zmiennych lub właściwości, których potrzebujesz. Oryginalny konstruktor TThread wymaga parametru o nazwie Suspended. Jak można się spodziewać, ustawienie Suspended = True zapobiegnie automatycznemu uruchomieniu wątku po utworzeniu. Jeśli Suspended = False, wątek zacznie działać zaraz po utworzeniu. Jeśli wątek zostanie utworzony jako zawieszony, zostanie uruchomiony dopiero po wywołaniu metody Start.
176172
177173> **Note:** Metoda Resume jest [przestarzała od FPC 2.4.4](http://wiki.freepascal.org/User_Changes_2.4.4#TThread.Suspend_and_TThread.Resume_have_been_deprecated). Została zastąpiona przez metodę Start.
178174
179175Począwszy od wersji FPC 2.0.1 i nowszych, TThread.Create ma również niejawny parametr dla rozmiaru stosu. W razie potrzeby możesz teraz zmienić domyślny rozmiar stosu każdego utworzonego wątku. Dobrym przykładem są głębokie rekurencje wywołań procedur w wątku. Jeśli nie określisz parametru rozmiaru stosu, zostanie użyty domyślny rozmiar stosu systemu operacyjnego.
180176
181177W zastąpionej metodzie Execute napisz kod, który będzie uruchamiany w wątku.
182178
183179Klasa TThread ma jedną ważną właściwość: Terminated : boolean;
184180
185181Jeśli wątek ma pętlę (i jest to typowe), pętla powinna zostać zakończona, gdy Terminated ma wartość true (domyślnie jest to false). W każdym przebiegu należy sprawdzić wartość Terminated, a jeśli jest prawdziwa, pętla powinna zostać zakończona tak szybko, jak to konieczne, po każdym koniecznym czyszczeniu. ~~Należy pamiętać, że metoda Terminate domyślnie nic nie robi: metoda .Execute musi jawnie zaimplementować obsługę, aby zakończyć swoje zadanie.~~ Metoda Terminate domyślnie ustawia jedynie właściwość Terminated na wartość **True**.
186182
187183Jak wyjaśniliśmy wcześniej, wątek nie powinien wchodzić w interakcje z widocznymi komponentami. Aktualizacje widocznych komponentów muszą być dokonywane w kontekście głównego wątku.
188184
189185Aby to zrobić, istnieje metoda TThread o nazwie Synchronize. Synchronize wymaga metody w wątku (która nie przyjmuje parametrów) jako argumentu. Po wywołaniu tej metody za pomocą Synchronize(@MyMethod) wykonanie wątku zostanie wstrzymane, kod MyMethod zostanie wywołany z głównego wątku, a następnie zostanie wznowione wykonywanie wątku.
190186
191187Dokładne działanie metody Synchronize zależy od platformy, ale zasadniczo wykonuje ona:
192188
193189- wysyła wiadomość do głównej kolejki wiadomości i zasypia
194190- ostatecznie główny wątek przetwarza wiadomość i wywołuje MyMethod. W ten sposób MyMethod jest wywoływana bez kontekstu, co oznacza, że ​​nie wykonuje się podczas zdarzenia wciśnięcia myszy lub podczas malowania, ale po.
195191- po wykonaniu przez główny wątek MyMethod, budzi ona uśpiony wątek i przetwarza następną wiadomość
196192- następnie wątek jest kontynuowany.
197193
198194Jest jeszcze jedna ważna właściwość TThread: FreeOnTerminate. Jeśli ta właściwość ma wartość true, obiekt wątku jest automatycznie zwalniany po zatrzymaniu wykonywania wątku (metoda .Execute). W przeciwnym razie aplikacja będzie musiała zwolnić ją ręcznie.
199195
200196Przykład:
201197
202198``` pascal
203199 Type
204200 TMyThread = class(TThread)
205201 private
206202 fStatusText : string;
207203 procedure ShowStatus;
208204 protected
209205 procedure Execute; override;
210206 public
211207 Constructor Create(CreateSuspended : boolean);
212208 end;
213209 constructor TMyThread.Create(CreateSuspended : boolean);
214210 begin
215211 inherited Create(CreateSuspended);
216212 FreeOnTerminate := True;
217213 end;
218214
219215 procedure TMyThread.ShowStatus;
220216 // ta metoda jest wykonywana przez główny wątek i dlatego może uzyskać dostęp do wszystkich elementów GUI.
221217 begin
222218 Form1.Caption := fStatusText;
223219 end;
224220
225221 procedure TMyThread.Execute;
226222 var
227223 newStatus : string;
228224 begin
229225 fStatusText := 'TMyThread Starting...';
230226 Synchronize(@Showstatus);
231227 fStatusText := 'TMyThread Running...';
232228 while (not Terminated) and ([inne wymagane warunki]) do
233229 begin
234230 ...
235231 [tutaj jest kod głównej pętli wątku]
236232 ...
237233 if NewStatus <> fStatusText then
238234 begin
239235 fStatusText := newStatus;
240236 Synchronize(@Showstatus);
241237 end;
242238 end;
243239 end;
244240```
245241
246242W aplikacji:
247243
248244``` pascal
249245 var
250246 MyThread : TMyThread;
251247 begin
252248 MyThread := TMyThread.Create(True); // W ten sposób nie uruchamia się automatycznie
253249 ...
254250 [Tutaj kod inicjuje wszystko, co jest wymagane, zanim wątki zaczną się wykonywać]
255251 ...
256252 MyThread.Start;
257253 end;
258254```
259255
260256Jeśli chcesz, aby Twoja aplikacja była bardziej elastyczna, możesz utworzyć zdarzenie dla wątku; w ten sposób Twoja zsynchronizowana metoda nie będzie ściśle powiązana z określoną formą lub klasą: możesz dołączyć detektory do zdarzenia wątku. Oto przykład:
261257
262258``` pascal
263259 Type
264260 TShowStatusEvent = procedure(Status: String) of Object;
265261
266262 TMyThread = class(TThread)
267263 private
268264 fStatusText : string;
269265 FOnShowStatus: TShowStatusEvent;
270266 procedure ShowStatus;
271267 protected
272268 procedure Execute; override;
273269 public
274270 Constructor Create(CreateSuspended : boolean);
275271 property OnShowStatus: TShowStatusEvent read FOnShowStatus write FOnShowStatus;
276272 end;
277273
278274 constructor TMyThread.Create(CreateSuspended : boolean);
279275 begin
280276 inherited Create(CreateSuspended);
281277 FreeOnTerminate := True;
282278 end;
283279
284280 procedure TMyThread.ShowStatus;
285281 // ta metoda jest wykonywana przez główny wątek i dlatego może uzyskać dostęp do wszystkich elementów GUI.
286282 begin
287283 if Assigned(FOnShowStatus) then
288284 begin
289285 FOnShowStatus(fStatusText);
290286 end;
291287 end;
292288
293289 procedure TMyThread.Execute;
294290 var
295291 newStatus : string;
296292 begin
297293 fStatusText := 'TMyThread Starting...';
298294 Synchronize(@Showstatus);
299295 fStatusText := 'TMyThread Running...';
300296 while (not Terminated) and ([inne wymagane warunki]) do
301297 begin
302298 ...
303299 [tutaj jest kod głównej pętli wątku]
304300 ...
305301 if NewStatus <> fStatusText then
306302 begin
307303 fStatusText := newStatus;
308304 Synchronize(@Showstatus);
309305 end;
310306 end;
311307 end;
312308```
313309
314310W aplikacji:
315311
316312``` pascal
317313 Type
318314 TForm1 = class(TForm)
319315 Button1: TButton;
320316 Label1: TLabel;
321317 procedure Button1Click(Sender: TObject);
322318 procedure FormCreate(Sender: TObject);
323319 procedure FormDestroy(Sender: TObject);
324320 private
325321 { private declarations }
326322 MyThread: TMyThread;
327323 procedure ShowStatus(Status: string);
328324 public
329325 { public declarations }
330326 end;
331327
332328 procedure TForm1.FormCreate(Sender: TObject);
333329 begin
334330 inherited;
335331 MyThread := TMyThread.Create(true);
336332 MyThread.OnShowStatus := @ShowStatus;
337333 end;
338334
339335 procedure TForm1.FormDestroy(Sender: TObject);
340336 begin
341337 MyThread.Terminate;
342338
343339 // FreeOnTerminate ma wartość true więc nie powinniśmy pisać:
344340 // MyThread.Free;
345341 inherited;
346342 end;
347343
348344 procedure TForm1.Button1Click(Sender: TObject);
349345 begin
350346 MyThread.Start;
351347 end;
352348
353349 procedure TForm1.ShowStatus(Status: string);
354350 begin
355351 Label1.Caption := Status;
356352 end;
357353```
358354
359355## Ważne rzeczy, na które trzeba zwrócić uwagę
360356
361357### Kontrola stosu w systemie Windows
362358
363359Może wystąpić potencjalny problem w systemie Windows, jeśli używasz przełącznika -Ct (sprawdzanie stosu). Z powodów, które nie są do końca jasne, sprawdzanie stosu będzie „wyzwalane” z każdym wywołaniem TThread.Create, jeśli używasz domyślnego rozmiaru stosu. W tej chwili jedynym rozwiązaniem jest po prostu nieużywanie przełącznika -Ct. Zauważ, że NIE powoduje to wyjątku w głównym wątku, ale w nowo utworzonym. To „wygląda” tak, jakby wątek nigdy nie został uruchomiony.
364360
365361Poprawny kod do sprawdzenia tego i innych wyjątków, które mogą wystąpić podczas tworzenia wątków, to:
366362
367363``` pascal
368364MyThread := TThread.Create(False);
369365
370366if Assigned(MyThread.FatalException) then
371367 raise MyThread.FatalException;
372368```
373369
374370Ten kod zapewni, że każdy wyjątek, który wystąpił podczas tworzenia wątku, zostanie zgłoszony w głównym wątku.
375371
376372### Wielowątkowość w pakietach
377373
378374Pakiety korzystające z wielowątkowości powinny dodawać flagę **-dUseCThreads** do dodatkowo używanych opcji. Otwórz edytor pakietów, a następnie Opcje \> Użycie \> Dodatkowe i dodaj *-dUseCThreads*. Spowoduje to zdefiniowanie tej flagi dla wszystkich projektów i pakietów korzystających z tego pakietu, w tym IDE. IDE i wszystkie nowe aplikacje utworzone przez IDE mają już następujący kod w swoim pliku .lpr:
379375
380376``` pascal
381377uses
382378 {$IFDEF UNIX}{$IFDEF UseCThreads}
383379 cthreads,
384380 cmem, // Menedżer pamięci c jest w niektórych systemach znacznie szybszy w przypadku wielowątkowości
385381 {$ENDIF}{$ENDIF}
386382```
387383
388384### Moduł Heaptrc
389385
390386Nie można używać przełącznika -gh z modułem *cmem*. Przełącznik -gh używa modułu heaptrc, który rozszerza menedżer stosu. Dlatego moduł **heaptrc** musi być używany **po** module **cmem**.
391387
392388``` pascal
393389uses
394390 {$IFDEF UNIX}{$IFDEF UseCThreads}
395391 cthreads,
396392 cmem, // Menedżer pamięci c jest w niektórych systemach znacznie szybszy w przypadku wielowątkowości
397393 {$ENDIF}{$ENDIF}
398394 heaptrc,
399395```
400396
401397### Initialization and Finalization
402398
403399Aby zainicjować sam obiekt wątku, możesz uruchomić go w trybie zawieszenia i ustawić jego właściwości i/lub utworzyć nowy konstruktor i wywołać odziedziczony konstruktor.
404400
405401**Uwaga**: Używanie AfterConstruction, gdy CreateSuspended=false jest niebezpieczne, ponieważ wątek już zaczął działać.
406402
407403Z drugiej strony, destruktor może służyć do finalizowania zasobów obiektu.
408404
409405``` pascal
410406type
411407 TMyThread = class(TThread)
412408 private
413409 fRTLEvent: PRTLEvent;
414410 public
415411 procedure Create(SomeData: TSomeObject); override;
416412 destructor Destroy; override;
417413 end;
418414
419415procedure TMyThread.Create(SomeData: TSomeObject; CreateSuspended: boolean);
420416begin
421417 // przykład: skonfiguruj zdarzenia, sekcje krytyczne i inne zasoby, takie jak pliki lub połączenia z bazami danych
422418 RTLEventCreate(fRTLEvent);
423419 inherited Create(CreateSuspended);
424420end;
425421
426422destructor TMyThread.Destroy;
427423begin
428424 RTLeventDestroy(fRTLEvent);
429425 inherited Destroy;
430426end;
431427```
432428
433429### Program bez LCL
434430
435431TThread.Synchronize wymaga, aby główny wątek regularnie wywoływał CheckSynchronize. LCL robi to w swojej pętli. Jeśli nie używasz pętli zdarzeń LCL, musisz ją wywołać samodzielnie.
436432
437433## Wsparcie dla SMP
438434
439435Dobrą wiadomością jest to, że jeśli Twoja aplikacja działa poprawnie wielowątkowo, to w ten sposób, jest już włączona obsługa SMP, czyli przetwarzanie wątków przez wiele procesorów!
440436
441437## Debugowanie aplikacji wielowątkowych w Lazarusie
442438
443439Debugowanie na Lazarusie wymaga [[GDB/pl|GDB]] i szybko staje się coraz bardziej funkcjonalne i stabilne. Jednak nadal istnieje kilka dystrybucji Linuksa z pewnymi problemami.
444440
445441### Wyjście debuggera
446442
447443W jednowątkowej aplikacji możesz po prostu pisać do konsoli/terminala/gdziekolwiek, a kolejność wierszy jest taka sama, jak zostały one napisane. W aplikacjach wielowątkowych sprawy są bardziej skomplikowane. Jeśli dwa wątki piszą, w taki sposób, że jakiś wiersz jest zapisywany przez wątek A przed wierszem przez wątek B, to wiersze niekoniecznie są zapisywane w tej kolejności. Może się nawet zdarzyć, że wątek zapisuje swoje wyjście, w tym samy czasie gdy drugi wątek zapisuje linię. W tej sytuacji pod Linuksem (być może) otrzymasz poprawne wyjście DebugLn(), a pod win32 możesz otrzymać wyjątek (prawdopodobnie DiskFull) z powodu użycia DebugLn() poza głównym wątkiem. Tak więc, aby uniknąć bólów głowy, użyj DebugLnThreadLog() wspomnianego poniżej.
448444
449445Moduł LCLProc zawiera kilka funkcji, które pozwalają każdemu wątkowi zapisywać do własnego pliku dziennika:
450446
451447``` pascal
452448 procedure DbgOutThreadLog(const Msg: string); overload;
453449 procedure DebuglnThreadLog(const Msg: string); overload;
454450 procedure DebuglnThreadLog(Args: array of const); overload;
455451 procedure DebuglnThreadLog; overload;
456452```
457453
458454Na przykład: Zamiast writeln('Jakiś tekst', 123); użyj
459455
460456` DebuglnThreadLog(['Jakiś tekst ',123]);`
461457
462458Spowoduje to dodanie linii 'Jakiś tekst 123' do **Log\<PID\>.txt**, gdzie \<PID\> jest identyfikatorem procesu bieżącego wątku.
463459
464460Dobrym pomysłem jest usunięcie plików dziennika przed każdym uruchomieniem:
465461
466462` rm -f Log* && ./project1`
467463
468464### Linux
469465
470466Jeśli spróbujesz debugować aplikację wielowątkową w systemie Linux, będziesz miał jeden duży problem: Menedżer pulpitu na serwerze X może się zawiesić. Dzieje się tak na przykład, gdy aplikacja przechwyciła mysz/klawiaturę i została zatrzymana przez gdb, a serwer X czeka na twoją aplikację. Kiedy tak się stanie, możesz po prostu zalogować się z innego komputera i zabić gdb lub wyjść z tej sesji, naciskając CTRL+ALT+F3 i zabić gdb. Alternatywnie możesz ponownie uruchomić menedżera okien: wpisz sudo /etc/init.d/gdm restart. Spowoduje to ponowne uruchomienie menedżera pulpitu i powrót do pulpitu.
471467
472468Ponieważ zależy to od tego, gdzie gdb zatrzymuje twój program, w niektórych przypadkach mogą pomóc pewne sztuczki: dla Ubuntu x64 ustaw opcje projektu do debugowania wymaganego dodatkowego pliku informacyjnego...
473469
474470` Projekt -> Opcje projektu -> Opcje kompilatora -> Odpluskwianie, i zaznacz: Use external gdb debug symbols file (-Xg).`
475471
476472Inną opcją jest otwarcie innego pulpitu X, uruchomienie IDE/gdb na jednym i aplikacji na drugim, tak aby zawieszał się tylko pulpit testowy. Utwórz nową instancję X za pomocą:
477473
478474` X :1 &`
479475
480476Gdy pulpit się otworzy, możesz przełączyć się na inny pulpit (ten, z którym pracujesz, naciskając CTRL + ALT + F7), będziesz mógł wrócić do nowego pulpitu graficznego za pomocą CTRL + ALT + F8 (jeśli ta kombinacja nie działa, spróbuj z CTRL + ALT + F2 ... ta kombinacja klawiszy działała w [Slackware](http://www.slackware.com)).
481477
482478Następnie możesz, jeśli chcesz, utworzyć sesję pulpitu na X, za pomocą polecenia:
483479
484480` gnome-session --display=:1 &`
485481
486482Następnie w Lazarusie w oknie dialogowym parametrów uruchamiania projektu (Uruchom -\> Uruchom z parametrami -\> Ekran) zaznacz opcję „Użyj ekranu” i wpisz :1.
487483
488484Teraz aplikacja będzie działać na drugim serwerze X i będziesz mógł ją debugować na pierwszym.
489485
490486Zostało to przetestowane z Free Pascal 2.0 i Lazarus 0.9.10 w systemie Linux.
491487
492488------------------------------------------------------------------------
493489
494490Zamiast tworzyć nową sesję X, można użyć Xnest. Xnest to sesja X w oknie. Korzystanie z niego sprawia, że X serwer nie blokuje się podczas debugowania wątków i jest znacznie łatwiej debugować bez konieczności zmiany terminali.
495491
496492Linia poleceń do uruchomienia Xnest to
497493
498494` Xnest :1 -ac`
499495
500496tworzy sesję X na :1 i wyłącza kontrolę dostępu.
501497
502498## Interfejsy widżetów Lazarusa
503499
504500Win32, gtk i interfejsy carbon obsługują wielowątkowość. Oznacza to, że działają tu TThread, krytyczne sekcje i Synchronize. Ale nie są bezpieczne wątkowo. To znaczy, że tylko jeden wątek na raz może uzyskać dostęp do LCL. Ponieważ główny wątek nigdy nie powinien czekać na kolejny wątek, to sprawia, że tylko wątek główny może uzyskać dostęp do LCL, czyli do wszystkiego, co ma związek z uchwytami TControl, Application i widgetów LCL. W LCL jest kilka funkcji bezpiecznych dla wątków. Na przykład większość funkcji w module FileUtil jest bezpieczna wątkowo.
505501
506502### Użycie SendMessage/PostMessage do komunikacji pomiędzy wątkami
507503
508504Tylko jeden wątek w aplikacji powinien wywoływać interfejsy API LCL, zwykle wątek główny. Inne wątki mogą korzystać z LCL za pomocą wielu metod pośrednich, jedną z dobrych opcji jest użycie SendMessage lub PostMessage. LCLIntf.SendMessage i LCLIntf.PostMessage wyślą wiadomość skierowaną do okna w puli wiadomości aplikacji.
509505
510506Zobacz także dokumentację tych procedur:
511507
512508- [[doc:lcl/lclintf/sendmessage.html|SendMessage]]
513509- [[doc:lcl/lclintf/postmessage.html|PostMessage]]
514510
515511Różnica między SendMessage i PostMessage polega na sposobie, w jaki zwracają kontrolę do wątku wywołującego. Podobnie jak dla Synchronize, w SendMessage bloki i kontrola nie są zwracane, dopóki okno, do którego wiadomość została wysłana, nie skończy jej przetwarzać; jednak w pewnych okolicznościach SendMessage może próbować zoptymalizować przetwarzanie, pozostając w kontekście wątku, który go wywołał. Dzięki PostMessage kontrola jest zwracana natychmiast do określonej przez system maksymalnej liczby umieszczonych w kolejce wiadomości i tak długo, jak na stercie pozostaje miejsce na dołączane dane.
516512
517513W obu przypadkach procedura obsługująca komunikat (patrz niżej) powinna unikać wywoływania application.ProcessMessages, ponieważ może to spowodować wysłanie drugiego komunikatu, który zostanie obsłużony ponownie. Jeśli jest to nieuniknione, prawdopodobnie lepiej byłoby użyć innego mechanizmu do przesyłania serializowanych zdarzeń między wątkami.
518514
519515Oto przykład, w jaki wątek dodatkowy może wysłać tekst, który ma być wyświetlany w kontrolce LCL do wątku głównego:
520516
521517``` pascal
522518const
523519 WM_GOT_ERROR = LM_USER + 2004;
524520 WM_VERBOSE = LM_USER + 2005;
525521
526522procedure VerboseLog(Msg: string);
527523var
528524 PError: PChar;
529525begin
530526 if MessageHandler = 0 then Exit;
531527 PError := StrAlloc(Length(Msg)+1);
532528 StrCopy(PError, PChar(Msg));
533529 PostMessage(formConsole.Handle, WM_VERBOSE, Integer(PError), 0);
534530end;
535531```
536532
537533I przykład, jak obsłużyć tę wiadomość z okna:
538534
539535``` pascal
540536const
541537 WM_GOT_ERROR = LM_USER + 2004;
542538 WM_VERBOSE = LM_USER + 2005;
543539
544540type
545541 { TformConsole }
546542
547543 TformConsole = class(TForm)
548544 DebugList: TListView;
549545 // ...
550546 private
551547 procedure HandleDebug(var Msg: TLMessage); message WM_VERBOSE;
552548 end;
553549
554550var
555551 formConsole: TformConsole;
556552
557553implementation
558554
559555....
560556
561557{ TformConsole }
562558
563559procedure TformConsole.HandleDebug(var Msg: TLMessage);
564560var
565561 Item: TListItem;
566562 MsgStr: PChar;
567563 MsgPasStr: string;
568564begin
569565 MsgStr := PChar(Msg.wparam);
570566 MsgPasStr := StrPas(MsgStr);
571567 Item := DebugList.Items.Add;
572568 Item.Caption := TimeToStr(SysUtils.Now);
573569 Item.SubItems.Add(MsgPasStr);
574570 Item.MakeVisible(False);
575571
576572// Po którym następuje coś takiego
577573
578574 TrayControl.SetError(MsgPasStr);
579575 StrDispose(MsgStr)
580576end;
581577
582578end.
583579```
584580
585581## Sekcje krytyczne
586582
587583*Sekcja krytyczna* to obiekt służący do upewnienia się, że jakaś część kodu jest wykonywana tylko przez jeden wątek na raz. Sekcja krytyczna musi zostać utworzona/zainicjowana, zanim będzie można jej użyć i zwolniona, gdy nie będzie już potrzebna.
588584
589585Sekcje krytyczne zwykle są używane w ten sposób:
590586
591587Deklaracja sekcji (globalnie dla wszystkich wątków, które powinny mieć dostęp do tej sekcji):
592588
593589` MyCriticalSection: TRTLCriticalSection;`
594590
595591Utworzenie sekcji:
596592
597593` InitializeCriticalSection(MyCriticalSection);`
598594
599595Uruchomienie kilka wątków. Robienie czegoś na wyłączność:
600596
601597``` pascal
602598EnterCriticalSection(MyCriticalSection);
603599try
604600 // uzyskaj dostęp do zmiennych, zapisz pliki, wyślij pakiety sieciowe itp.
605601finally
606602 LeaveCriticalSection(MyCriticalSection);
607603end;
608604```
609605
610606Po zakończeniu wszystkich wątków zwolnij sekcję krytyczną:
611607
612608` DeleteCriticalSection(MyCriticalSection);`
613609
614610Alternatywnie możesz użyć obiektu TCriticalSection. Inicjalizację wykonujemy przez jego utworzenie, metoda Enter wykonuje EnterCriticalSection, metoda Leave wykonuje LeaveCriticalSection, a zniszczenie obiektu powoduje jego usunięcie.
615611
616612Należy zauważyć, że sekcja krytyczna nie chroni przed wprowadzeniem tego samego wątku do tego samego bloku kodu, tylko przed różnymi wątkami. Z tego powodu nie może służyć do ochrony m.in. przed ponownym wprowadzeniem do programu obsługi wiadomości (patrz sekcja wyżej).
617613
618614Na przykład: 5 wątków zwiększających licznik. Zobacz lazarus/examples/multithreading/criticalsectionexample1.lpi
619615
620616> **Warning:** Istnieją dwa zestawy powyższych czterech funkcji. Znajdują się w RTL i LCL. Te w LCL są zdefiniowane w modułach LCLIntf i LCLType. Oba działają prawie tak samo. Możesz używać obu jednocześnie w swojej aplikacji, ale nie powinieneś używać funkcji RTL w sekcji krytycznej LCL i na odwrót.
621617
622618### Współdzielenie zmiennych
623619
624620Jeśli niektóre wątki współdzielą zmienną, która jest tylko do odczytu, nie ma się czym martwić. Po prostu odczytaj jej wartość. Ale jeśli jeden lub kilka wątków zmienia wartość zmiennej, to musisz upewnić się, że tylko jeden wątek ma dostęp do tej zmiennej w danej chwili.
625621
626622Na przykład: 5 wątków zwiększających licznik. Zobacz lazarus/examples/multithreading/criticalsectionexample1.lpi
627623
628624## Oczekiwanie na inny wątek
629625
630626Jeśli wątek A potrzebuje wyniku innego wątku B, musi on poczekać, aż B zakończy pracę.
631627
632628**Ważne:** Główny wątek nigdy nie powinien czekać na kolejny wątek. Zamiast tego użyj opcji Synchronizuj (patrz wyżej).
633629
634630Zobacz przykład: lazarus/examples/multithreading/waitforexample1.lpi
635631
636632``` pascal
637633{ TThreadA }
638634
639635procedure TThreadA.Execute;
640636begin
641637 Form1.ThreadB:=TThreadB.Create(false);
642638 // Utwórz zdarzenie
643639 WaitForB:=RTLEventCreate;
644640 while not Application.Terminated do begin
645641 // czekaj w nieskończoność (aż B obudzi A)
646642 RtlEventWaitFor(WaitForB);
647643 writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter));
648644 end;
649645end;
650646
651647{ TThreadB }
652648
653649procedure TThreadB.Execute;
654650var
655651 i: Integer;
656652begin
657653 Counter:=0;
658654 while not Application.Terminated do begin
659655 // B: Pracuje ...
660656 Sleep(1500);
661657 inc(Counter);
662658 // wybudza A
663659 RtlEventSetEvent(Form1.ThreadA.WaitForB);
664660 end;
665661end;
666662```
667663
668664> **Note:** RtlEventSetEvent można wywołać przed RtlEventWaitFor. Wtedy RtlEventWaitFor natychmiast powróci. Użyj RLeventResetEvent, aby wyczyścić flagę.
669665
670666## Wątek potomny, rozwidlenie (fork)
671667
672668Podczas rozwidlenia w aplikacji wielowątkowej należy pamiętać, że wszelkie wątki utworzone i uruchomione PRZED wywołaniem fork (lub fpFork) NIE będą uruchomione w procesie potomnym. Jak stwierdzono na stronie podręcznika fork(), wszelkie wątki, które działały przed wywołaniem fork, będą miały stan niezdefiniowany.
673669
674670Należy więc pamiętać o wszelkich wątkach inicjujących się przed wywołaniem fork (w tym w sekcji inicjowania). NIE będą działać.
675671
676672## Procedury/pętle równoległe
677673
678674Szczególnym przypadkiem wielowątkowości jest równoległe uruchamianie pojedynczej procedury. Zobacz [[Parallel procedures|Procedury równoległe]].
679675
680676## Obliczenia rozproszone
681677
682678Następnym wyższym krokiem po wielowątkowości jest uruchamianie wątków na wielu komputerach.
683679
684680- Do komunikacji można użyć jednego z pakietów TCP, takich jak synapse, lnet lub indy. Daje to maksymalną elastyczność i jest używane głównie w przypadku luźno połączonych aplikacji Klient/Serwer.
685681- Możesz użyć bibliotek przekazujących komunikaty, takich jak [[MPICH]], które są używane do HPC (High Performance Computing) w klastrach.
686682
687683## Wątki zewnętrzne
688684
689685Aby system wątków Free Pascal działał poprawnie, każdy nowo utworzony wątek FPC musi zostać zainicjowany (dokładniej muszą być zainicjowane: wyjątek, system I/O i system zmiennych dla wątków, aby działały zmienne wątków i stos). Jest to wykonywane w pełni automatycznie, jeśli używasz BeginThread (lub pośrednio za pomocą klasy TThread). Jeśli jednak używasz wątków, które zostały utworzone bez BeginThread (tj. wątków zewnętrznych), może być wymagana dodatkowa praca (obecnie). Wątki zewnętrzne obejmują również te, które zostały utworzone w zewnętrznych bibliotekach C (.DLL/.so).
690686
691687Kwestie do rozważenia podczas korzystania z wątków zewnętrznych (mogą nie być potrzebne we wszystkich lub przyszłych wersjach kompilatora):
692688
693689- Nie używaj w ogóle wątków zewnętrznych - używaj wątków FPC. Jeśli możesz uzyskać kontrolę nad sposobem tworzenia wątku, utwórz wątek samodzielnie za pomocą BeginThread.
694690
695691Jeśli konwencja wywoływania nie pasuje (np. jeśli oryginalna funkcja wątku wymaga konwencji wywoływania cdecl, ale BeginThread wymaga konwencji Pascala, utwórz rekord, w którym przechowasz oryginalną wymaganą funkcję wątku i wywołaj tę funkcję w funkcji wątku Pascala:
696692
697693``` pascal
698694type
699695 TCdeclThreadFunc = function (user_data:Pointer):Pointer;cdecl;
700696
701697 PCdeclThreadFuncData = ^TCdeclThreadFuncData;
702698 TCdeclThreadFuncData = record
703699 Func: TCdeclThreadFunc; //funkcja cdecl
704700 Data: Pointer; //oryginalne dane
705701 end;
706702
707703// Wątek Pascala wywołuje funkcję cdecl
708704function C2P_Translator(FuncData: pointer) : ptrint;
709705var
710706 ThreadData: TCdeclThreadFuncData;
711707begin
712708 ThreadData := PCdeclThreadFuncData(FuncData)^;
713709 Result := ptrint(ThreadData.Func(ThreadData.Data));
714710end;
715711
716712procedure CreatePascalThread;
717713var
718714 ThreadData: PCdeclThreadFuncData;
719715begin
720716 New(ThreadData);
721717 // to jest pożądana funkcja wątku cdecl
722718 ThreadData^.Func := func;
723719 ThreadData^.Data := user_data;
724720 // to tworzy wątek Pascala
725721 BeginThread(@C2P_Translator, ThreadData );
726722end;
727723```
728724
729725- Zainicjuj system wątków FPC, tworząc fikcyjny wątek. Jeśli nie utworzysz żadnego wątku Pascala w swojej aplikacji, system wątków nie zostanie zainicjowany (a zatem zmienne wątków nie będą działać, a zatem i stos nie będzie działać poprawnie).
730726
731727``` pascal
732728type
733729 tc = class(tthread)
734730 procedure execute;override;
735731 end;
736732
737733 procedure tc.execute;
738734 begin
739735 end;
740736
741737{ main program }
742738begin
743739 { zainicjować system wątków }
744740 with tc.create(false) do
745741 begin
746742 waitfor;
747743 free;
748744 end;
749745 { ... Twój kod wykonywany dalej }
750746end.
751747```
752748
753749(Po zainicjowaniu systemu wątków środowisko wykonawcze może ustawić zmienną systemową „IsMultiThread” na wartość true, która jest używana przez procedury FPC do wykonywania blokad tu i tam. Nie należy ustawiać tej zmiennej ręcznie.)
754750
755751- Jeśli z jakiegoś powodu to nie działa, wypróbuj ten kod w funkcji wątku zewnętrznego:
756752
757753``` pascal
758754function ExternalThread(param: Pointer): LongInt; stdcall;
759755var
760756 tm: TThreadManager;
761757begin
762758 GetThreadManager(tm);
763759 tm.AllocateThreadVars;
764760 InitThread(1000000); // dostosuj tutaj początkowy rozmiar stosu
765761
766762 { zrób tutaj coś w wątku ... }
767763
768764 Result:=0;
769765end;
770766```
771767
772768### Identyfikacja wątków zewnętrznych
773769
774770Czasami nawet nie wiesz, czy masz do czynienia z wątkami zewnętrznymi (np. czy jakaś biblioteka C wykonuje wywołanie zwrotne). To może pomóc w analizie takiej sytuacji:
775771
7767721\. Zapytaj system operacyjny o identyfikator bieżącego wątku podczas uruchamiania aplikacji
777773
778774``` pascal
779775GetCurrentThreadID() //windows;
780776GetThreadID() //Darwin/macOS;
781777TThreadID(pthread_self) //Linux;
782778```
783779
7847802\. Zapytaj ponownie o identyfikator bieżącego wątku wewnątrz funkcji wątku i porównaj to z wynikiem kroku 1.
785781
786782### Dodawanie opóźnień czasowych
787783
788784ThreadSwitch()
789785
790786> **Note:** Nie używaj sztuczki z uśpieniem w Windows *Sleep(0)* ponieważ nie będzie to działać na wszystkich platformach.
791787
792788## Zobacz także
793789
794790- [[Streaming components|Komponenty strumieniowe]]
795791- [[Manager Worker Threads System|System nadzorowania wątków roboczych]]
796792- [[Example of multi-threaded application: array of threads/pl|Przykład aplikacji wielowątkowej: tablicy wątków]]
797793- [[Parallel procedures|Procedury równoległe]]
798794- [[Main Loop Hooks|Petla gówna uchwytów]]
799795- [[Asynchronous Calls|Wywołanie asynchroniczne]]
800796- [Mutlithreading - The Delphi way](http://nickhodges.com/MultiThreadingInDelphi/ToC.html)
801797
802798---
803799*From the Free Pascal wiki, page "Multithreaded Application Tutorial/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).*

Back to history