Sign in Join
Wiki copytemporary

Multithreaded Application Tutorial/pl: what changed

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

OldNewLine
11Kurs tworzenia aplikacji wielowątkowych
22
33## Wprowadzenie
44
55Na 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.
66
77Pierwszy 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ć.
88
99Podstawowym 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.
1010
1111Innym 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.
1212
1313Jeszcze innym zastosowaniem wielowątkowości może być serwer, który jest w stanie dać odpowiedź wielu klientom, w tym samym czasie.
1414
1515## Czy potrzebujesz wielowątkowości?
1616
1717Jeś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.
1818
1919Przykład: Czytanie dużego pliku i jego przetwarzanie. Zobacz: examples/multithreading/singlethreadingexample1.lpi.
2020
2121Wielowątkowość jest potrzebna tylko w przypadku
2222
2323- używania uchwytów blokujących, takich jak w komunikacji sieci
2424- korzystania z wielu procesorów jednocześnie (SMP)
2525- algorytmów i bibliotek, które muszą być wywoływane przez API i jako takie nie mogą być podzielone na mniejsze części.
2626
2727Jeś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.
2828
2929## Moduły potrzebne do tworzenia aplikacji wielowątkowej
3030
3131Nie 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)!
3232
3333Dlatego twój kod aplikacji Lazarusa powinien wyglądać tak:
3434
3535``` pascal
3636program MyMultiThreadedProgram;
3737{$mode objfpc}{$H+}
3838uses
3939{$ifdef unix}
4040 cthreads,
4141 cmem, // menedżer pamięci c jest w niektórych systemach znacznie szybszy dla aplikacji wielowątkowych
4242{$endif}
4343 Interfaces, // to zawiera widżety LCL
4444 Forms
4545 { tu możesz dodać następne moduły },
4646```
4747
4848Jeśli o tym zapomnisz i użyjesz TThread, otrzymasz następujący błąd podczas uruchamiania:
4949
50-` This binary has no thread support compiled in.`\
51-` Recompile the application with a thread-driver in the program uses clause before other units using thread.`
50+``` pascal
51+ This binary has no thread support compiled in.
52+ Recompile the application with a thread-driver in the program uses clause before other units using thread.
53+```
5254
5355> **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]].
5456
5557> **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.
5658
5759## Przykład w czystym FPC
5860
5961Poniższy kod przedstawia bardzo prosty przykład. Testowany z FPC 3.0.4 na Win7.
6062
6163``` pascal
6264Program ThreadTest;
6365{ przetestuj możliwość aplikacji wielowątkowej }
6466{
6567 OUTPUT
6668wątek 1 uruchomiony
6769wątek 1 zmienna thri 0 Długość(s)= 1
6870wątek 1 zmienna thri 1 Długość(s)= 2
6971wątek 1 zmienna thri 2 Długość(s)= 3
7072wątek 1 zmienna thri 3 Długość(s)= 4
7173wątek 1 zmienna thri 4 Długość(s)= 5
7274wątek 1 zmienna thri 5 Długość(s)= 6
7375wątek 1 zmienna thri 6 Długość(s)= 7
7476wątek 1 zmienna thri 7 Długość(s)= 8
7577wątek 1 zmienna thri 8 Długość(s)= 9
7678wątek 1 zmienna thri 9 Długość(s)= 10
7779wątek 1 zmienna thri 10 Długość(s)= 11
7880wątek 1 zmienna thri 11 Długość(s)= 12
7981wątek 1 zmienna thri 12 Długość(s)= 13
8082wątek 1 zmienna thri 13 Długość(s)= 14
8183wątek 1 zmienna thri 14 Długość(s)= 15
8284wątek 2 uruchomiony
8385wątek 3 uruchomiony
8486wątek 1 zmienna thri 15 Długość(s)= 16
8587wątek 2 zmienna thri 0 Długość(s)= 1
8688wątek 3 zmienna thri 0 Długość(s)= 1
8789wątek 1 zmienna thri 16 Długość(s)= 17
8890...
8991...
9092wątek 5 zmienna thri 997 Długość(s)= 998
9193wątek 5 zmienna thri 998 Długość(s)= 999
9294wątek 5 zmienna thri 999 Długość(s)= 1000
9395wątek 5 zakończony
9496wątek 10 zmienna thri 828 Długość(s)= 829
9597wątek 9 zmienna thri 675 Długość(s)= 676
9698wątek 4 zmienna thri 656 Długość(s)= 657
9799wątek 10 zmienna thri 829 Długość(s)= 830
98100wątek 9 zmienna thri 676 Długość(s)= 677
99101wątek 9 zmienna thri 677 Długość(s)= 678
100102wątek 10 zmienna thri 830 Długość(s)= 831
101103wątek 10 zmienna thri 831 Długość(s)= 832
102104wątek 10 zmienna thri 832 Długość(s)= 833
103105wątek 10 zmienna thri 833 Długość(s)= 834
104106wątek 10 zmienna thri 834 Długość(s)= 835
105107wątek 10 zmienna thri 835 Długość(s)= 836
106108wątek 10 zmienna thri 836 Długość(s)= 837
107109wątek 10 zmienna thri 837 Długość(s)= 838
108110wątek 10 zmienna thri 838 Długość(s)= 839
109111wątek 10 zmienna thri 839 Długość(s)= 840
110112wątek 9 zmienna thri 678 Długość(s)= 679
111113...
112114...
113115wątek 4 zmienna thri 994 Długość(s)= 995
114116wątek 4 zmienna thri 995 Długość(s)= 996
115117wątek 4 zmienna thri 996 Długość(s)= 997
116118wątek 4 zmienna thri 997 Długość(s)= 998
117119wątek 4 zmienna thri 998 Długość(s)= 999
118120wątek 4 zmienna thri 999 Długość(s)= 1000
119121wątek 4 zakończony
12012210
121123
122124}
123125
124126uses
125127 {$ifdef unix}cthreads, {$endif} sysutils;
126128
127129const
128130 threadcount = 10;
129131 stringlen = 1000;
130132
131133var
132134 finished : longint;
133135
134136threadvar
135137 thri : ptrint;
136138
137139function f(p : pointer) : ptrint;
138140var
139141 s : ansistring;
140142begin
141143 Writeln('wątek ',longint(p),' uruchomiony');
142144 thri:=0;
143145 while (thri<stringlen) do begin
144146 s:=s+'1'; { stwórz opóźnienie }
145147 writeln('wątek ',longint(p),' zmienna thri ',thri,' Długość(s)= ',length(s));
146148 inc(thri);
147149 end;
148150 Writeln('wątek ',longint(p),' zakończony');
149151 InterLockedIncrement(finished);
150152 f:=0;
151153end;
152154
153155var
154156 i : longint;
155157
156158Begin
157159 finished:=0;
158160 for i:=1 to threadcount do
159161 BeginThread(@f,pointer(i));
160162 while finished<threadcount do ;
161163 Writeln(finished);
162164End.
163165```
164166
165167## Klasa TThread
166168
167169Poniższy przykład można znaleźć w katalogu examples/multithreading/.
168170
169171Aby 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.
170172
171173W 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.
172174
173175> **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.
174176
175177Począ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.
176178
177179W zastąpionej metodzie Execute napisz kod, który będzie uruchamiany w wątku.
178180
179181Klasa TThread ma jedną ważną właściwość: Terminated : boolean;
180182
181183Jeś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**.
182184
183185Jak 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.
184186
185187Aby 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.
186188
187189Dokładne działanie metody Synchronize zależy od platformy, ale zasadniczo wykonuje ona:
188190
189191- wysyła wiadomość do głównej kolejki wiadomości i zasypia
190192- 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.
191193- po wykonaniu przez główny wątek MyMethod, budzi ona uśpiony wątek i przetwarza następną wiadomość
192194- następnie wątek jest kontynuowany.
193195
194196Jest 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.
195197
196198Przykład:
197199
198200``` pascal
199201 Type
200202 TMyThread = class(TThread)
201203 private
202204 fStatusText : string;
203205 procedure ShowStatus;
204206 protected
205207 procedure Execute; override;
206208 public
207209 Constructor Create(CreateSuspended : boolean);
208210 end;
209211 constructor TMyThread.Create(CreateSuspended : boolean);
210212 begin
211213 inherited Create(CreateSuspended);
212214 FreeOnTerminate := True;
213215 end;
214216
215217 procedure TMyThread.ShowStatus;
216218 // ta metoda jest wykonywana przez główny wątek i dlatego może uzyskać dostęp do wszystkich elementów GUI.
217219 begin
218220 Form1.Caption := fStatusText;
219221 end;
220222
221223 procedure TMyThread.Execute;
222224 var
223225 newStatus : string;
224226 begin
225227 fStatusText := 'TMyThread Starting...';
226228 Synchronize(@Showstatus);
227229 fStatusText := 'TMyThread Running...';
228230 while (not Terminated) and ([inne wymagane warunki]) do
229231 begin
230232 ...
231233 [tutaj jest kod głównej pętli wątku]
232234 ...
233235 if NewStatus <> fStatusText then
234236 begin
235237 fStatusText := newStatus;
236238 Synchronize(@Showstatus);
237239 end;
238240 end;
239241 end;
240242```
241243
242244W aplikacji:
243245
244246``` pascal
245247 var
246248 MyThread : TMyThread;
247249 begin
248250 MyThread := TMyThread.Create(True); // W ten sposób nie uruchamia się automatycznie
249251 ...
250252 [Tutaj kod inicjuje wszystko, co jest wymagane, zanim wątki zaczną się wykonywać]
251253 ...
252254 MyThread.Start;
253255 end;
254256```
255257
256258Jeś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:
257259
258260``` pascal
259261 Type
260262 TShowStatusEvent = procedure(Status: String) of Object;
261263
262264 TMyThread = class(TThread)
263265 private
264266 fStatusText : string;
265267 FOnShowStatus: TShowStatusEvent;
266268 procedure ShowStatus;
267269 protected
268270 procedure Execute; override;
269271 public
270272 Constructor Create(CreateSuspended : boolean);
271273 property OnShowStatus: TShowStatusEvent read FOnShowStatus write FOnShowStatus;
272274 end;
273275
274276 constructor TMyThread.Create(CreateSuspended : boolean);
275277 begin
276278 inherited Create(CreateSuspended);
277279 FreeOnTerminate := True;
278280 end;
279281
280282 procedure TMyThread.ShowStatus;
281283 // ta metoda jest wykonywana przez główny wątek i dlatego może uzyskać dostęp do wszystkich elementów GUI.
282284 begin
283285 if Assigned(FOnShowStatus) then
284286 begin
285287 FOnShowStatus(fStatusText);
286288 end;
287289 end;
288290
289291 procedure TMyThread.Execute;
290292 var
291293 newStatus : string;
292294 begin
293295 fStatusText := 'TMyThread Starting...';
294296 Synchronize(@Showstatus);
295297 fStatusText := 'TMyThread Running...';
296298 while (not Terminated) and ([inne wymagane warunki]) do
297299 begin
298300 ...
299301 [tutaj jest kod głównej pętli wątku]
300302 ...
301303 if NewStatus <> fStatusText then
302304 begin
303305 fStatusText := newStatus;
304306 Synchronize(@Showstatus);
305307 end;
306308 end;
307309 end;
308310```
309311
310312W aplikacji:
311313
312314``` pascal
313315 Type
314316 TForm1 = class(TForm)
315317 Button1: TButton;
316318 Label1: TLabel;
317319 procedure Button1Click(Sender: TObject);
318320 procedure FormCreate(Sender: TObject);
319321 procedure FormDestroy(Sender: TObject);
320322 private
321323 { private declarations }
322324 MyThread: TMyThread;
323325 procedure ShowStatus(Status: string);
324326 public
325327 { public declarations }
326328 end;
327329
328330 procedure TForm1.FormCreate(Sender: TObject);
329331 begin
330332 inherited;
331333 MyThread := TMyThread.Create(true);
332334 MyThread.OnShowStatus := @ShowStatus;
333335 end;
334336
335337 procedure TForm1.FormDestroy(Sender: TObject);
336338 begin
337339 MyThread.Terminate;
338340
339341 // FreeOnTerminate ma wartość true więc nie powinniśmy pisać:
340342 // MyThread.Free;
341343 inherited;
342344 end;
343345
344346 procedure TForm1.Button1Click(Sender: TObject);
345347 begin
346348 MyThread.Start;
347349 end;
348350
349351 procedure TForm1.ShowStatus(Status: string);
350352 begin
351353 Label1.Caption := Status;
352354 end;
353355```
354356
355357## Ważne rzeczy, na które trzeba zwrócić uwagę
356358
357359### Kontrola stosu w systemie Windows
358360
359361Moż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.
360362
361363Poprawny kod do sprawdzenia tego i innych wyjątków, które mogą wystąpić podczas tworzenia wątków, to:
362364
363365``` pascal
364366MyThread := TThread.Create(False);
365367
366368if Assigned(MyThread.FatalException) then
367369 raise MyThread.FatalException;
368370```
369371
370372Ten kod zapewni, że każdy wyjątek, który wystąpił podczas tworzenia wątku, zostanie zgłoszony w głównym wątku.
371373
372374### Wielowątkowość w pakietach
373375
374376Pakiety 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:
375377
376378``` pascal
377379uses
378380 {$IFDEF UNIX}{$IFDEF UseCThreads}
379381 cthreads,
380382 cmem, // Menedżer pamięci c jest w niektórych systemach znacznie szybszy w przypadku wielowątkowości
381383 {$ENDIF}{$ENDIF}
382384```
383385
384386### Moduł Heaptrc
385387
386388Nie 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**.
387389
388390``` pascal
389391uses
390392 {$IFDEF UNIX}{$IFDEF UseCThreads}
391393 cthreads,
392394 cmem, // Menedżer pamięci c jest w niektórych systemach znacznie szybszy w przypadku wielowątkowości
393395 {$ENDIF}{$ENDIF}
394396 heaptrc,
395397```
396398
397399### Initialization and Finalization
398400
399401Aby 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.
400402
401403**Uwaga**: Używanie AfterConstruction, gdy CreateSuspended=false jest niebezpieczne, ponieważ wątek już zaczął działać.
402404
403405Z drugiej strony, destruktor może służyć do finalizowania zasobów obiektu.
404406
405407``` pascal
406408type
407409 TMyThread = class(TThread)
408410 private
409411 fRTLEvent: PRTLEvent;
410412 public
411413 procedure Create(SomeData: TSomeObject); override;
412414 destructor Destroy; override;
413415 end;
414416
415417procedure TMyThread.Create(SomeData: TSomeObject; CreateSuspended: boolean);
416418begin
417419 // przykład: skonfiguruj zdarzenia, sekcje krytyczne i inne zasoby, takie jak pliki lub połączenia z bazami danych
418420 RTLEventCreate(fRTLEvent);
419421 inherited Create(CreateSuspended);
420422end;
421423
422424destructor TMyThread.Destroy;
423425begin
424426 RTLeventDestroy(fRTLEvent);
425427 inherited Destroy;
426428end;
427429```
428430
429431### Program bez LCL
430432
431433TThread.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.
432434
433435## Wsparcie dla SMP
434436
435437Dobrą 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!
436438
437439## Debugowanie aplikacji wielowątkowych w Lazarusie
438440
439441Debugowanie na Lazarusie wymaga [[GDB/pl|GDB]] i szybko staje się coraz bardziej funkcjonalne i stabilne. Jednak nadal istnieje kilka dystrybucji Linuksa z pewnymi problemami.
440442
441443### Wyjście debuggera
442444
443445W 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.
444446
445447Moduł LCLProc zawiera kilka funkcji, które pozwalają każdemu wątkowi zapisywać do własnego pliku dziennika:
446448
447449``` pascal
448450 procedure DbgOutThreadLog(const Msg: string); overload;
449451 procedure DebuglnThreadLog(const Msg: string); overload;
450452 procedure DebuglnThreadLog(Args: array of const); overload;
451453 procedure DebuglnThreadLog; overload;
452454```
453455
454456Na przykład: Zamiast writeln('Jakiś tekst', 123); użyj
455457
456-` DebuglnThreadLog(['Jakiś tekst ',123]);`
458+```text
459+ DebuglnThreadLog(['Jakiś tekst ',123]);
460+```
457461
458462Spowoduje to dodanie linii 'Jakiś tekst 123' do **Log\<PID\>.txt**, gdzie \<PID\> jest identyfikatorem procesu bieżącego wątku.
459463
460464Dobrym pomysłem jest usunięcie plików dziennika przed każdym uruchomieniem:
461465
462-` rm -f Log* && ./project1`
466+```text
467+ rm -f Log* && ./project1
468+```
463469
464470### Linux
465471
466472Jeś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.
467473
468474Ponieważ 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...
469475
470-` Projekt -> Opcje projektu -> Opcje kompilatora -> Odpluskwianie, i zaznacz: Use external gdb debug symbols file (-Xg).`
476+```text
477+ Projekt -> Opcje projektu -> Opcje kompilatora -> Odpluskwianie, i zaznacz: Use external gdb debug symbols file (-Xg).
478+```
471479
472480Inną 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ą:
473481
474-` X :1 &`
482+```text
483+ X :1 &
484+```
475485
476486Gdy 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)).
477487
478488Następnie możesz, jeśli chcesz, utworzyć sesję pulpitu na X, za pomocą polecenia:
479489
480-` gnome-session --display=:1 &`
490+```text
491+ gnome-session --display=:1 &
492+```
481493
482494Następnie w Lazarusie w oknie dialogowym parametrów uruchamiania projektu (Uruchom -\> Uruchom z parametrami -\> Ekran) zaznacz opcję „Użyj ekranu” i wpisz :1.
483495
484496Teraz aplikacja będzie działać na drugim serwerze X i będziesz mógł ją debugować na pierwszym.
485497
486498Zostało to przetestowane z Free Pascal 2.0 i Lazarus 0.9.10 w systemie Linux.
487499
488500------------------------------------------------------------------------
489501
490502Zamiast 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.
491503
492504Linia poleceń do uruchomienia Xnest to
493505
494-` Xnest :1 -ac`
506+```text
507+ Xnest :1 -ac
508+```
495509
496510tworzy sesję X na :1 i wyłącza kontrolę dostępu.
497511
498512## Interfejsy widżetów Lazarusa
499513
500514Win32, 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.
501515
502516### Użycie SendMessage/PostMessage do komunikacji pomiędzy wątkami
503517
504518Tylko 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.
505519
506520Zobacz także dokumentację tych procedur:
507521
508522- [[doc:lcl/lclintf/sendmessage.html|SendMessage]]
509523- [[doc:lcl/lclintf/postmessage.html|PostMessage]]
510524
511525Róż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.
512526
513527W 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.
514528
515529Oto 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:
516530
517531``` pascal
518532const
519533 WM_GOT_ERROR = LM_USER + 2004;
520534 WM_VERBOSE = LM_USER + 2005;
521535
522536procedure VerboseLog(Msg: string);
523537var
524538 PError: PChar;
525539begin
526540 if MessageHandler = 0 then Exit;
527541 PError := StrAlloc(Length(Msg)+1);
528542 StrCopy(PError, PChar(Msg));
529543 PostMessage(formConsole.Handle, WM_VERBOSE, Integer(PError), 0);
530544end;
531545```
532546
533547I przykład, jak obsłużyć tę wiadomość z okna:
534548
535549``` pascal
536550const
537551 WM_GOT_ERROR = LM_USER + 2004;
538552 WM_VERBOSE = LM_USER + 2005;
539553
540554type
541555 { TformConsole }
542556
543557 TformConsole = class(TForm)
544558 DebugList: TListView;
545559 // ...
546560 private
547561 procedure HandleDebug(var Msg: TLMessage); message WM_VERBOSE;
548562 end;
549563
550564var
551565 formConsole: TformConsole;
552566
553567implementation
554568
555569....
556570
557571{ TformConsole }
558572
559573procedure TformConsole.HandleDebug(var Msg: TLMessage);
560574var
561575 Item: TListItem;
562576 MsgStr: PChar;
563577 MsgPasStr: string;
564578begin
565579 MsgStr := PChar(Msg.wparam);
566580 MsgPasStr := StrPas(MsgStr);
567581 Item := DebugList.Items.Add;
568582 Item.Caption := TimeToStr(SysUtils.Now);
569583 Item.SubItems.Add(MsgPasStr);
570584 Item.MakeVisible(False);
571585
572586// Po którym następuje coś takiego
573587
574588 TrayControl.SetError(MsgPasStr);
575589 StrDispose(MsgStr)
576590end;
577591
578592end.
579593```
580594
581595## Sekcje krytyczne
582596
583597*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.
584598
585599Sekcje krytyczne zwykle są używane w ten sposób:
586600
587601Deklaracja sekcji (globalnie dla wszystkich wątków, które powinny mieć dostęp do tej sekcji):
588602
589-` MyCriticalSection: TRTLCriticalSection;`
603+```text
604+ MyCriticalSection: TRTLCriticalSection;
605+```
590606
591607Utworzenie sekcji:
592608
593-` InitializeCriticalSection(MyCriticalSection);`
609+```text
610+ InitializeCriticalSection(MyCriticalSection);
611+```
594612
595613Uruchomienie kilka wątków. Robienie czegoś na wyłączność:
596614
597615``` pascal
598616EnterCriticalSection(MyCriticalSection);
599617try
600618 // uzyskaj dostęp do zmiennych, zapisz pliki, wyślij pakiety sieciowe itp.
601619finally
602620 LeaveCriticalSection(MyCriticalSection);
603621end;
604622```
605623
606624Po zakończeniu wszystkich wątków zwolnij sekcję krytyczną:
607625
608-` DeleteCriticalSection(MyCriticalSection);`
626+```text
627+ DeleteCriticalSection(MyCriticalSection);
628+```
609629
610630Alternatywnie 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.
611631
612632Należ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).
613633
614634Na przykład: 5 wątków zwiększających licznik. Zobacz lazarus/examples/multithreading/criticalsectionexample1.lpi
615635
616636> **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.
617637
618638### Współdzielenie zmiennych
619639
620640Jeś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.
621641
622642Na przykład: 5 wątków zwiększających licznik. Zobacz lazarus/examples/multithreading/criticalsectionexample1.lpi
623643
624644## Oczekiwanie na inny wątek
625645
626646Jeśli wątek A potrzebuje wyniku innego wątku B, musi on poczekać, aż B zakończy pracę.
627647
628648**Ważne:** Główny wątek nigdy nie powinien czekać na kolejny wątek. Zamiast tego użyj opcji Synchronizuj (patrz wyżej).
629649
630650Zobacz przykład: lazarus/examples/multithreading/waitforexample1.lpi
631651
632652``` pascal
633653{ TThreadA }
634654
635655procedure TThreadA.Execute;
636656begin
637657 Form1.ThreadB:=TThreadB.Create(false);
638658 // Utwórz zdarzenie
639659 WaitForB:=RTLEventCreate;
640660 while not Application.Terminated do begin
641661 // czekaj w nieskończoność (aż B obudzi A)
642662 RtlEventWaitFor(WaitForB);
643663 writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter));
644664 end;
645665end;
646666
647667{ TThreadB }
648668
649669procedure TThreadB.Execute;
650670var
651671 i: Integer;
652672begin
653673 Counter:=0;
654674 while not Application.Terminated do begin
655675 // B: Pracuje ...
656676 Sleep(1500);
657677 inc(Counter);
658678 // wybudza A
659679 RtlEventSetEvent(Form1.ThreadA.WaitForB);
660680 end;
661681end;
662682```
663683
664684> **Note:** RtlEventSetEvent można wywołać przed RtlEventWaitFor. Wtedy RtlEventWaitFor natychmiast powróci. Użyj RLeventResetEvent, aby wyczyścić flagę.
665685
666686## Wątek potomny, rozwidlenie (fork)
667687
668688Podczas 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.
669689
670690Należ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ć.
671691
672692## Procedury/pętle równoległe
673693
674694Szczególnym przypadkiem wielowątkowości jest równoległe uruchamianie pojedynczej procedury. Zobacz [[Parallel procedures|Procedury równoległe]].
675695
676696## Obliczenia rozproszone
677697
678698Następnym wyższym krokiem po wielowątkowości jest uruchamianie wątków na wielu komputerach.
679699
680700- 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.
681701- Możesz użyć bibliotek przekazujących komunikaty, takich jak [[MPICH]], które są używane do HPC (High Performance Computing) w klastrach.
682702
683703## Wątki zewnętrzne
684704
685705Aby 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).
686706
687707Kwestie do rozważenia podczas korzystania z wątków zewnętrznych (mogą nie być potrzebne we wszystkich lub przyszłych wersjach kompilatora):
688708
689709- 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.
690710
691711Jeś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:
692712
693713``` pascal
694714type
695715 TCdeclThreadFunc = function (user_data:Pointer):Pointer;cdecl;
696716
697717 PCdeclThreadFuncData = ^TCdeclThreadFuncData;
698718 TCdeclThreadFuncData = record
699719 Func: TCdeclThreadFunc; //funkcja cdecl
700720 Data: Pointer; //oryginalne dane
701721 end;
702722
703723// Wątek Pascala wywołuje funkcję cdecl
704724function C2P_Translator(FuncData: pointer) : ptrint;
705725var
706726 ThreadData: TCdeclThreadFuncData;
707727begin
708728 ThreadData := PCdeclThreadFuncData(FuncData)^;
709729 Result := ptrint(ThreadData.Func(ThreadData.Data));
710730end;
711731
712732procedure CreatePascalThread;
713733var
714734 ThreadData: PCdeclThreadFuncData;
715735begin
716736 New(ThreadData);
717737 // to jest pożądana funkcja wątku cdecl
718738 ThreadData^.Func := func;
719739 ThreadData^.Data := user_data;
720740 // to tworzy wątek Pascala
721741 BeginThread(@C2P_Translator, ThreadData );
722742end;
723743```
724744
725745- 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).
726746
727747``` pascal
728748type
729749 tc = class(tthread)
730750 procedure execute;override;
731751 end;
732752
733753 procedure tc.execute;
734754 begin
735755 end;
736756
737757{ main program }
738758begin
739759 { zainicjować system wątków }
740760 with tc.create(false) do
741761 begin
742762 waitfor;
743763 free;
744764 end;
745765 { ... Twój kod wykonywany dalej }
746766end.
747767```
748768
749769(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.)
750770
751771- Jeśli z jakiegoś powodu to nie działa, wypróbuj ten kod w funkcji wątku zewnętrznego:
752772
753773``` pascal
754774function ExternalThread(param: Pointer): LongInt; stdcall;
755775var
756776 tm: TThreadManager;
757777begin
758778 GetThreadManager(tm);
759779 tm.AllocateThreadVars;
760780 InitThread(1000000); // dostosuj tutaj początkowy rozmiar stosu
761781
762782 { zrób tutaj coś w wątku ... }
763783
764784 Result:=0;
765785end;
766786```
767787
768788### Identyfikacja wątków zewnętrznych
769789
770790Czasami 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:
771791
7727921\. Zapytaj system operacyjny o identyfikator bieżącego wątku podczas uruchamiania aplikacji
773793
774794``` pascal
775795GetCurrentThreadID() //windows;
776796GetThreadID() //Darwin/macOS;
777797TThreadID(pthread_self) //Linux;
778798```
779799
7808002\. Zapytaj ponownie o identyfikator bieżącego wątku wewnątrz funkcji wątku i porównaj to z wynikiem kroku 1.
781801
782802### Dodawanie opóźnień czasowych
783803
784804ThreadSwitch()
785805
786806> **Note:** Nie używaj sztuczki z uśpieniem w Windows *Sleep(0)* ponieważ nie będzie to działać na wszystkich platformach.
787807
788808## Zobacz także
789809
790810- [[Streaming components|Komponenty strumieniowe]]
791811- [[Manager Worker Threads System|System nadzorowania wątków roboczych]]
792812- [[Example of multi-threaded application: array of threads/pl|Przykład aplikacji wielowątkowej: tablicy wątków]]
793813- [[Parallel procedures|Procedury równoległe]]
794814- [[Main Loop Hooks|Petla gówna uchwytów]]
795815- [[Asynchronous Calls|Wywołanie asynchroniczne]]
796816- [Mutlithreading - The Delphi way](http://nickhodges.com/MultiThreadingInDelphi/ToC.html)
797817
798818---
799819*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