Sign in Join
Wiki copytemporary

Multithreaded Application Tutorial/ru: what changed

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

OldNewLine
11## Обзор
22
33Эта страница объясняет как писать и отлаживать многопоточные(multi-threaded) приложения средствами Free Pascal и Lazarus. Многопоточное приложение — это приложение, которое создаёт два или более потока исполнения, работающих одновременно. Если Вы новичок в многопоточности, пожалуйста, прочитайте раздел "Нужна ли на самом деле многопоточность?" чтобы определить, что это действительно необходимо.
44
55Один из потоков называется главным, основным (Main Thread). Главный поток создаётся операционной системой после запуска приложения. Главный поток **должен быть** единственным потоком, который обновляет компоненты, взаимодействующие с пользователем: иначе приложение может зависнуть.
66
77Основная идея состоит в том, что приложение может производить некоторую обработку в фоновом режиме во втором потоке, а пользователь может продолжать работу с помощью основного потока.
88
99Другое использование потоков состоит в лучшей отзывчивости (responding) приложения. Если вы создали приложение, где пользователь нажимает на кнопку, обрабатывающую большой объём работы, во время обработки экран перестаёт отвечать на запросы и пользователь думает, что приложение не отвечает. Создаётся плохое и вводящее в заблуждение впечатление. Если это задание выполняется во втором потоке, приложение сохраняет работоспособную форму (почти), как будто оно находится в состоянии простоя. В этом смысле это хорошая идея перед запуском потока выключить кнопки на форме, позволяющие пользователю запустить более чем один поток для работы.
1010
1111Другим способом использования многопоточности может быть серверное приложение, которое одновременно отвечает многим клиентам.
1212
1313## Нужна ли на самом деле многопоточность?
1414
1515Если Вы новичок в многопоточности и хотите только чтобы Ваше приложение отвечало на действия пользователя, пока оно выполняет задачи умеренной длительности, тогда мультипоточность, возможно, это избыточное средство.
1616
1717Многопоточные приложения всегда более сложные в отладке и зачастую они имеют гораздо более сложную структуру; во многих случаях Вам не нужна многопоточность. Одного потока вполне достаточно. Если Вы можете разделить трудоёмкую задачу на несколько небольших частей, тогда вместо многопоточности Вы должны использовать **Application.ProcessMessages**. Этот метод позволяет LCL обрабатывать все ожидающие сообщений и возвращения (messages and returns). Центральной идеей является вызов **Application.ProcessMessages** в одинаковые промежутки в ходе выполнения длительной задачи чтобы определить, нажал ли пользователь куда-нибудь или индикатор прогресса должен быть перерисован, и так далее.
1818
1919К примеру, чтение большого файла и его обработки См. *examples/multithreading/singlethreadingexample1.lpi*.
2020
2121Многопоточность необходима только для:
2222
2323- блокировки handles, таких как сетевое соединение
2424- использования нескольких процессоров одновременно (multiple processors simultaneously, SMP)
2525- вызовы алгоритмов и библиотек, которые должны вызываться через API, т.к. они не могут быть разделены на мелкие части.
2626
2727## Модули, необходимые для многопоточного приложения
2828
2929Вам не нужен какой-либо специальный модуль для работы с Windows. Однако с Linux, macOS и FreeBSD вам нужен модуль `cthreads`, и он *должен* быть первым используемым модулем проекта (в исходнике программы, обычно файл .lpr)! В случаях, когда потребуется разместить одновременно несколько "важных" модулей, таких как `cthreads`, `cmem` и `cwstrings`, ввиду особенностей их работы оптимальным порядком размещения будет `cmem`, `cthreads`, и лишь затем `cwstrings`.
3030
3131Итак, код вашего приложения Lazarus должен выглядеть так:
3232
3333``` pascal
3434program MyMultiThreadedProgram;
3535{$mode objfpc}{$H+}
3636uses
3737{$ifdef unix}
3838 cthreads,
3939 cmem, // диспетчер памяти C в некоторых системах намного быстрее для многопоточности
4040{$endif}
4141 Interfaces, // это включает в себя набор виджетов LCL
4242 Forms
4343 { Вы можете добавить другие модули здесь },
4444```
4545
4646Если вы забудете об этом и используете TThread (не добавив для nix-ов cthreads), вы получите эту ошибку при запуске:
4747
4848` This binary has no thread support compiled in.`\
4949` Recompile the application with a thread-driver in the program uses clause before other units using thread.`\
5050` { В этом двоичном файле нет поддержки потоков.}`\
5151` {Перекомпилируйте приложение с драйвером потока, указанном в разделе uses программы перед другими модулями, использующими поток.}`
5252
5353> **Note:** Если вы получаете ошибку компоновщика о том, что "mcount" не найден, значит, вы используете какой-то модуль, содержащий многопоточный код, и вам нужно добавить модуль `cthreads` или использовать интеллектуальное связывание.
5454
5555> **Note:** Если вы получаете ошибку: "Project raised exception class 'RunError(232)'" в процедуре SYSTEM_NOTHREADERROR, то ваш код требует многопоточности и вам необходимо добавить модуль cthreads.
5656
5757## Простейший пример FPC
5858
5959Приведенный ниже код дает очень простой пример. Проверено с помощью FPC 3.0.4 на Win7.
6060
6161``` pascal
6262Program ThreadTest;
6363{тестирование возможности многопоточности}
6464{
6565 OUTPUT
6666thread 1 started
6767thread 1 thri 0 Len(S)= 1
6868thread 1 thri 1 Len(S)= 2
6969thread 1 thri 2 Len(S)= 3
7070thread 1 thri 3 Len(S)= 4
7171thread 1 thri 4 Len(S)= 5
7272thread 1 thri 5 Len(S)= 6
7373thread 1 thri 6 Len(S)= 7
7474thread 1 thri 7 Len(S)= 8
7575thread 1 thri 8 Len(S)= 9
7676thread 1 thri 9 Len(S)= 10
7777thread 1 thri 10 Len(S)= 11
7878thread 1 thri 11 Len(S)= 12
7979thread 1 thri 12 Len(S)= 13
8080thread 1 thri 13 Len(S)= 14
8181thread 1 thri 14 Len(S)= 15
8282thread 2 started
8383thread 3 started
8484thread 1 thri 15 Len(S)= 16
8585thread 2 thri 0 Len(S)= 1
8686thread 3 thri 0 Len(S)= 1
8787thread 1 thri 16 Len(S)= 17
8888...
8989...
9090thread 5 thri 997 Len(S)= 998
9191thread 5 thri 998 Len(S)= 999
9292thread 5 thri 999 Len(S)= 1000
9393thread 5 finished
9494thread 10 thri 828 Len(S)= 829
9595thread 9 thri 675 Len(S)= 676
9696thread 4 thri 656 Len(S)= 657
9797thread 10 thri 829 Len(S)= 830
9898thread 9 thri 676 Len(S)= 677
9999thread 9 thri 677 Len(S)= 678
100100thread 10 thri 830 Len(S)= 831
101101thread 10 thri 831 Len(S)= 832
102102thread 10 thri 832 Len(S)= 833
103103thread 10 thri 833 Len(S)= 834
104104thread 10 thri 834 Len(S)= 835
105105thread 10 thri 835 Len(S)= 836
106106thread 10 thri 836 Len(S)= 837
107107thread 10 thri 837 Len(S)= 838
108108thread 10 thri 838 Len(S)= 839
109109thread 10 thri 839 Len(S)= 840
110110thread 9 thri 678 Len(S)= 679
111111...
112112...
113113thread 4 thri 994 Len(S)= 995
114114thread 4 thri 995 Len(S)= 996
115115thread 4 thri 996 Len(S)= 997
116116thread 4 thri 997 Len(S)= 998
117117thread 4 thri 998 Len(S)= 999
118118thread 4 thri 999 Len(S)= 1000
119119thread 4 finished
12012010
121121
122122}
123123
124124uses
125125 {$ifdef unix}cthreads, {$endif} sysutils;
126126
127127const
128128 threadcount = 10;
129129 stringlen = 1000;
130130
131131var
132132 finished : longint;
133133
134134threadvar
135135 thri : ptrint;
136136
137137function f(p : pointer) : ptrint;
138138var
139139 s : ansistring;
140140begin
141141 Writeln('thread ',longint(p),' started');
142142 thri:=0;
143143 while (thri<stringlen) do begin
144144 s:=s+'1'; { create a delay }
145145 writeln('thread ',longint(p),' thri ',thri,' Len(S)= ',length(s));
146146 inc(thri);
147147 end;
148148 Writeln('thread ',longint(p),' finished');
149149 InterLockedIncrement(finished);
150150 f:=0;
151151end;
152152
153153var
154154 i : longint;
155155
156156Begin
157157 finished:=0;
158158 for i:=1 to threadcount do
159159 BeginThread(@f,pointer(i));
160160 while finished<threadcount do ;
161161 Writeln(finished);
162162End.
163163```
164164
165165## Класс TThread
166166
167167Самый простой путь для создания многопоточного приложения — использование **класса TThread**. Этот класс позволяет достаточно просто создать дополнительный поток (наряду с основным). Обычно от вас требуется переопределить только 2 метода: **конструктор Create**, и **метод Execute**.
168168
169169В **конструкторе** Вы подготавливаете поток для запуска. Вы устанавливаете начальные значения переменных и свойств, которые Вам нужны. Подлинный конструктор TThread требует параметр, называющийся **Suspended** (приостановить). Как и следует ожидать, установка Suspended = True будет препятствовать автоматическому запуску потоков после создания. Если Suspended = False, поток запускается сразу после создания. Если поток создан приостановленным (Suspended = True), тогда он запускается только после вызова **метода Resume**.
170170
171171В FPC version 2.0.1 и более поздних, TThread.Create также имеет неявный параметр, отвечающий за размер стека. Теперь, если это необходимо, Вы можете изменить стандартный размер стека каждого потока, который Вы создали. Глубокая рекурсия в процедурах является хорошим примером. Если Вы не изменяете размер стека, используется стандартный размер стека операционной системы.
172172
173173В переопределённом **методе Execute** Вы будете писать код, который запускается в потоке.
174174
175175Класс TThread имеет одно важное свойство: **Terminated** : Boolean;
176176
177177Если поток имеет цикл (loop) — и это нормально, — выход из цикла должен осуществиться, когда **Terminated = True** (по умолчанию – False). В каждом проходе значение свойство Terminated должно проверяться, и если оно True, цикл должен завершиться так быстро, как это возможно, после необходимой очистки. Имейте ввиду, что метод **Terminate** по умолчанию ничего не делает: в метод **Execute** нужно явно организовать завершение задания.
178178
179179Как мы объяснили ранее, поток не должен взаимодействовать с видимыми компонентами. Обновления для видимой компоненты должны быть сделаны в контексте главного, основного потока. Чтобы это сделать, у TThread существует метод, называющийся **Synchronize**. Synchronize требует метод в качестве аргумента. Когда вы вызываете метод через **Synchronize(@MyMethod)**, запущенный поток приостанавливается, код метода **MyMethod** запускается в главном потоке, а затем выполнение потока будет продолжено. Точная обработка Synchronize зависит от платформы, но упрощённо он делает следующее: он посылает сообщение в главную очередь сообщений и приостанавливает своё выполнение(goes to sleep). В конечном итоге главный поток обрабатывает сообщение и вызывает **MyMethod**. Таким образом, **MyMethod** вызывается без контекста, то есть не во время события **OnMouseDown** или в течение события перерисовки (paint event), но после этого. После того, как главный поток запускает **MyMethod**, он будит спящий поток и обрабатывает следующее сообщение из главной очереди сообщений, а поток продолжает работать.
180180
181181Существует другое важное свойство TThread: **FreeOnTerminate**. Если оно True, тогда объект потока автоматически освобождается, когда выполнение потока (метод **Execute**) останавливается. В противном случае, приложению будет необходимо освободить его вручную.
182182
183183Следующий пример может быть найден в каталоге *examples/multithreading/*:
184184
185185``` pascal
186186 Type
187187 TMyThread = class(TThread)
188188 private
189189 fStatusText : string;
190190 procedure ShowStatus;
191191 protected
192192 procedure Execute; override;
193193 public
194194 Constructor Create(CreateSuspended : boolean);
195195 end;
196196
197197 constructor TMyThread.Create(CreateSuspended : boolean);
198198 begin
199199 FreeOnTerminate := True;
200200 inherited Create(CreateSuspended);
201201 end;
202202
203203 procedure TMyThread.ShowStatus;
204204 // этот метод запущен главным потоком и поэтому может получить доступ ко всем элементам графического интерфейса.
205205 begin
206206 Form1.Caption := fStatusText;
207207 end;
208208
209209 procedure TMyThread.Execute;
210210 var
211211 newStatus : string;
212212 begin
213213 fStatusText := 'TMyThread Starting...';
214214 Synchronize(@Showstatus);
215215 fStatusText := 'TMyThread Running...';
216216 while (not Terminated) and ([любое необходимое условие]) do
217217 begin
218218 ...
219219 {здесь расположен код цикла главного (основного) потока}
220220 ...
221221 if NewStatus <> fStatusText then
222222 begin
223223 fStatusText := newStatus;
224224 Synchronize(@Showstatus);
225225 end;
226226 end;
227227 end;
228228```
229229
230230В приложении
231231
232232``` pascal
233233 var
234234 MyThread : TMyThread;
235235 begin
236236 MyThread := TMyThread.Create(True); // Таким способом он не запустится автоматически
237237 ...
238238 {Здесь расположен код, который инициализирует всё возможное перед запуском потоков}
239239 ...
240240 MyThread.Resume;
241241 end;
242242```
243243
244244Если вы хотите, чтобы ваши приложения были более гибкими, Вы можете создавать события для потока; таким образом, Ваши синхронизированные методы не будут тесно связаны с определенной формой или классом: Вы можете прикрепить слушателей событий потока. Вот пример:
245245
246246``` pascal
247247 Type
248248 TShowStatusEvent = procedure(Status: String) of Object;
249249
250250 TMyThread = class(TThread)
251251 private
252252 fStatusText : string;
253253 FOnShowStatus: TShowStatusEvent;
254254 procedure ShowStatus;
255255 protected
256256 procedure Execute; override;
257257 public
258258 Constructor Create(CreateSuspended : boolean);
259259 property OnShowStatus: TShowStatusEvent read FOnShowStatus write FOnShowStatus;
260260 end;
261261
262262 constructor TMyThread.Create(CreateSuspended : boolean);
263263 begin
264264 FreeOnTerminate := True;
265265 inherited Create(CreateSuspended);
266266 end;
267267
268268 procedure TMyThread.ShowStatus;
269269 // этот метод запущен главным потоком и поэтому может получить доступ ко всем элементам графического интерфейса.
270270 begin
271271 if Assigned(FOnShowStatus) then
272272 begin
273273 FOnShowStatus(fStatusText);
274274 end;
275275 end;
276276
277277 procedure TMyThread.Execute;
278278 var
279279 newStatus : string;
280280 begin
281281 fStatusText := 'TMyThread Starting...';
282282 Synchronize(@Showstatus);
283283 fStatusText := 'TMyThread Running...';
284284 while (not Terminated) and ([любое необходимое условие]) do
285285 begin
286286 ...
287287 {здесь расположен код цикла главного (основного) потока}
288288 ...
289289 if NewStatus <> fStatusText then
290290 begin
291291 fStatusText := newStatus;
292292 Synchronize(@Showstatus);
293293 end;
294294 end;
295295 end;
296296```
297297
298298В приложении
299299
300300``` pascal
301301 Type
302302 TForm1 = class(TForm)
303303 Button1: TButton;
304304 Label1: TLabel;
305305 procedure FormCreate(Sender: TObject);
306306 procedure FormDestroy(Sender: TObject);
307307 private
308308 { private declarations }
309309 MyThread: TMyThread;
310310 procedure ShowStatus(Status: string);
311311 public
312312 { public declarations }
313313 end;
314314
315315 procedure TForm1.FormCreate(Sender: TObject);
316316 begin
317317 inherited;
318318 MyThread := TMyThread.Create(true);
319319 MyThread.OnShowStatus := @ShowStatus;
320320 end;
321321
322322 procedure TForm1.FormDestroy(Sender: TObject);
323323 begin
324324 MyThread.Terminate;
325325 MyThread.Free;
326326 inherited;
327327 end;
328328
329329 procedure TForm1.Button1Click(Sender: TObject);
330330 begin
331331 MyThread.Resume;
332332 end;
333333
334334 procedure TForm1.ShowStatus(Status: string);
335335 begin
336336 Label1.Caption := Status;
337337 end;
338338```
339339
340340## Важно знать
341341
342342### Проверка стека в Windows
343343
344344Существует потенциальная проблема с потоками в Windows, если Вы используете переключатель (?) -Ct (stack check). По причинам не совсем понятным проверка стека "переключается" при любом TThread.Create если Вы используете размер стек по-умолчанию.
345345
346346Единственный работающий способ на данный момент - просто не использовать переключатель -Ct. Заметим, что это не вызывает исключения в основном потоке, но вызывает в недавно созданном. Это "выглядит" как если бы поток никогда не был запущен.
347347
348348Хороший код для проверки этого и других исключений, которые могут возникнуть в создании потоков:
349349
350350``` pascal
351351 MyThread:=TThread.Create(False);
352352 if Assigned(MyThread.FatalException) then
353353 raise MyThread.FatalException;
354354```
355355
356356Этот код будет гарантировать, что любое исключение, которое произошло во время создания потока будет распространёно на главный поток.
357357
358358### Функция GetDC на Windows
359359
360360Функция GetDC (и GetDCEx) извлекает хэндл контекста устройства (DC) для клиентской области указанного окна или для всего экрана. Ее можно использовать, например, для [[Developing with Graphics|создания снимка экрана]]. Однако обратите внимание, что эта функция не является потокобезопасной: хэндл к DC может использоваться только одним потоком в любой момент времени, как [описано здесь](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdc).
361361
362362### Многопоточность в пакетах
363363
364364Пакеты, использующие многопоточность, должны добавлять флаг **-dUseCThreads** в пользовательские параметры использования. Откройте редактор пакета, затем Options \> Usage \> Custom и добавьте *-dUseCThreads*. Это определит данный флаг для всех проектов и пакетов, использующих этот пакет, включая IDE. IDE и все новые приложения, созданные IDE, уже имеют следующий код в своем .lpr-файле:
365365
366366``` pascal
367367uses
368368 {$IFDEF UNIX}{$IFDEF UseCThreads}
369369 cthreads,
370370 cmem, // менеджер памяти Си в некоторых системах намного быстрее для многопоточности
371371 {$ENDIF}{$ENDIF}
372372```
373373
374374### Heaptrc
375375
376376Вы не можете использовать ключ -gh с модулем *cmem*. Ключ -gh использует модуль heaptrc, который расширяет менеджер кучи. Поэтому модуль **heaptrc** должен указываться **после** модуля **cmem**.
377377
378378``` pascal
379379uses
380380 {$IFDEF UNIX}{$IFDEF UseCThreads}
381381 cthreads,
382382 cmem, // менеджер памяти Си в некоторых системах намного быстрее для многопоточности
383383 {$ENDIF}{$ENDIF}
384384 heaptrc,
385385```
386386
387387### Initialization и Finalization
388388
389389Чтобы инициализировать сам объект потока, вы можете либо запустить его в приостановленном состоянии и задать его свойства и/или создать новый конструктор и вызвать унаследованный конструктор.
390390
391391**Примечание**: Использование AfterConstruction при `CreateSuspended=false` опасно, так как поток уже запущен.
392392
393393С другой стороны, деструктор может быть использован для завершения использования ресурсов объекта.
394394
395395``` pascal
396396type
397397 TMyThread = class(TThread)
398398 private
399399 fRTLEvent: PRTLEvent;
400400 public
401401 procedure Create(SomeData: TSomeObject); override;
402402 destructor Destroy; override;
403403 end;
404404
405405procedure TMyThread.Create(SomeData: TSomeObject; CreateSuspended: boolean);
406406begin
407407 // например: настройка событий, критических секций и других ресурсов, таких как файлы или соединения с базой данных
408408 RTLEventCreate(fRTLEvent);
409409 inherited Create(CreateSuspended);
410410end;
411411
412412destructor TMyThread.Destroy;
413413begin
414414 RTLeventDestroy(fRTLEvent);
415415 inherited Destroy;
416416end;
417417```
418418
419419### Не LCL-программы
420420
421421TThread.Synchronize требует, чтобы главный поток регулярно вызывал CheckSynchronize. LCL делает это в своем цикле. Если вы не используете цикл событий LCL, вы должны вызывать его самостоятельно.
422422
423423## Поддержка SMP
424424
425425Хорошей новостью является то, что если ваше приложение работает все-таки правильно в многопоточном режиме, то оно уже поддерживает SMP!
426426
427427## Отладка многопоточных приложений с помощью Lazarus
428428
429429Отладка на Lazarus требует GDB и быстро становится все более полнофункциональной и стабильной. Однако до сих пор существует несколько дистрибутивов Linux с некоторыми проблемами.
430430
431431### Вывод отладочной информации
432432
433433В однопоточном приложении вы можете просто писать в консоль/терминал/что угодно, и порядок строк будет таким же, как они были записаны. В многопоточном приложении все сложнее. Если пишут два потока, скажем, строка написана потоком A перед строкой потока B, то строки не обязательно будут записаны в таком порядке. В то время как в linux (возможно) вы получите правильный вывод [[DebugLn]](), то в win32 вы можете получить исключения (возможно DiskFull) из-за использования DebugLn() вне основного потока. Поэтому, чтобы избежать головной боли, используйте DebugLnThreadLog(), упомянутый ниже.
434434
435435Модуль LCLProc содержит несколько функций, позволяющих каждому потоку писать в свой собственный файл журнала:
436436
437437``` pascal
438438 procedure DbgOutThreadLog(const Msg: string); overload;
439439 procedure DebuglnThreadLog(const Msg: string); overload;
440440 procedure DebuglnThreadLog(Args: array of const); overload;
441441 procedure DebuglnThreadLog; overload;
442442```
443443
444444Например: Вместо *writeln('Some text ',123);* задействуйте
445445
446446` DebuglnThreadLog(['Some text ',123]);`
447447
448448Это добавит строку 'Some text 123' в **Log\<PID\>.txt**, где \<PID\> - идентификатор процесса текущего потока.
449449
450450Хорошей идеей будет удаление файлов журнала перед каждым запуском:
451451
452452` rm -f Log* && ./project1`
453453
454454### Linux
455455
456-Если вы попытаетесь отладить многопоточное приложение в Linux, вы столкнетесь с одной большой проблемой: менеджер рабочего стола на X-сервере может зависнуть. Это происходит, например, когда приложение захватило мышь/клавиатуру и было приостановлено gdb, а X-сервер ждет вашего приложения. Когда это происходит, вы можете просто войти в систему с другого компьютера и убить gdb или выйти из этой сессии, нажав <span class="kbd">CTRL</span>+<span class="kbd">ALT</span>+<span class="kbd">F3</span> и убить gdb. В качестве альтернативы вы можете перезапустить оконный менеджер: введите
456+Если вы попытаетесь отладить многопоточное приложение в Linux, вы столкнетесь с одной большой проблемой: менеджер рабочего стола на X-сервере может зависнуть. Это происходит, например, когда приложение захватило мышь/клавиатуру и было приостановлено gdb, а X-сервер ждет вашего приложения. Когда это происходит, вы можете просто войти в систему с другого компьютера и убить gdb или выйти из этой сессии, нажав `CTRL`+`ALT`+`F3` и убить gdb. В качестве альтернативы вы можете перезапустить оконный менеджер: введите
457457
458458` sudo /etc/init.d/gdm restart. `
459459
460460Это перезапустит менеджер рабочего стола и вернет вас на рабочий стол.
461461
462462Поскольку это зависит от того, где gdb останавливает вашу программу, в некоторых случаях могут помочь некоторые хитрости: для Ubuntu x64 установите в опциях проекта для отладки требуемый файл дополнительной информации...
463463
464464` Project Options -> Compiler Options -> Linking -> Debugging: Check Use external gdb debug symbols file (-Xg).`
465465
466466Другой вариант - открыть другой рабочий стол X, запустить IDE/gdb на одном и приложение на другом, чтобы зависал только тестовый рабочий стол. Создайте новый экземпляр X с:
467467
468468` X :1 &`
469469
470-Он откроется, и когда вы переключитесь на другой рабочий стол (тот, с которым вы работаете, нажав <span class="kbd">CTRL</span>+<span class="kbd">ALT</span>+<span class="kbd">F7</span>), вы сможете вернуться на новый графический рабочий стол с помощью <span class="kbd">CTRL</span>+<span class="kbd">ALT</span>+<span class="kbd">F8</span> (если эта комбинация не работает, попробуйте <span class="kbd">CTRL</span>+<span class="kbd">ALT</span>+<span class="kbd">F2</span>... этот вариант работал на [Slackware](http://www.slackware.com)).
470+Он откроется, и когда вы переключитесь на другой рабочий стол (тот, с которым вы работаете, нажав `CTRL`+`ALT`+`F7`), вы сможете вернуться на новый графический рабочий стол с помощью `CTRL`+`ALT`+`F8` (если эта комбинация не работает, попробуйте `CTRL`+`ALT`+`F2`... этот вариант работал на [Slackware](http://www.slackware.com)).
471471
472472Затем вы можете, если хотите, создать сеанс рабочего стола на X, с которого начался сеанс:
473473
474474` gnome-session --display=:1 &`
475475
476476Затем в Lazarus, в диалоге параметров запуска проекта, отметьте "Use display" и введите :1.
477477
478478Теперь приложение будет запущено на втором X-сервере, и вы сможете отлаживать его на первом.
479479
480480Это было проверено с Free Pascal 2.0 и Lazarus 0.9.10 на Windows и Linux.
481481
482482------------------------------------------------------------------------
483483
484484Вместо создания новой X-сессии можно использовать [Xnest](http://en.wikipedia.org/wiki/Xnest). Xnest - это X-сессия в окне. Используя его, X-сервер не блокируется при отладке потоков, и отлаживать гораздо проще, не меняя терминалы.
485485
486486Командная строка для запуска Xnest выглядит следующим образом
487487
488488` Xnest :1 -ac`
489489
490490для создания X-сессии на :1 и отключения контроля доступа.
491491
492492## Интерфейсы виджетов Lazarus
493493
494494Интерфейсы win32, gtk и carbon поддерживают многопоточность. Это означает, что TThread, критические секции и Synchronize работают. Но они не являются потокобезопасными. Это означает, что только один поток в одно время может получить доступ к LCL. А поскольку главный поток никогда не должен ждать другого потока, это означает, что только главный поток имеет доступ к LCL, то есть ко всему, что связано с TControl, Application и дескрипторами виджетов LCL. В LCL есть несколько потокобезопасных функций. Например, большинство функций в блоке FileUtil являются потокобезопасными.
495495
496496### Использование SendMessage/PostMessage для взаимодействия между потоками
497497
498498Только один поток в приложении должен вызывать API LCL, обычно это главный поток. Другие потоки могут использовать LCL с помощью ряда косвенных методов, одним из хороших вариантов является использование SendMessage или PostMessage. LCLIntf.SendMessage и LCLIntf.PostMessage отправляют сообщение в окно пула сообщений приложения.
499499
500500См. также документацию по этим процедурам:
501501
502502- [[doc:lcl/lclintf/sendmessage.html|SendMessage]]
503503- [[doc:lcl/lclintf/postmessage.html|PostMessage]]
504504
505505Разница между SendMessage и PostMessage заключается в том, как они возвращают управление вызывающему потоку. Как и Synchronize, SendMessage блокирует и управление не возвращается, пока окно, в которое было отправлено сообщение, не завершит его обработку; однако при определенных обстоятельствах SendMessage может попытаться оптимизировать обработку, оставаясь в контексте вызвавшего его потока. При PostMessage управление возвращается немедленно до некоторого определенного системой максимального количества поставленных в очередь сообщений и до тех пор, пока в куче остается место для вложенных данных.
506506
507507В обоих случаях процедура, обрабатывающая сообщение (см. ниже), должна избегать вызова application.ProcessMessages, поскольку это может привести к отправке второго сообщения, которое будет обрабатываться реентерабельно. Если это неизбежно, то предпочтительнее использовать другой механизм для передачи сериализованных событий между потоками.
508508
509509Вот пример того, как вторичный поток может посылать текст для отображения в элементе управления LCL главному потоку:
510510
511511``` pascal
512512const
513513 WM_GOT_ERROR = LM_USER + 2004;
514514 WM_VERBOSE = LM_USER + 2005;
515515
516516procedure VerboseLog(Msg: string);
517517var
518518 PError: PChar;
519519begin
520520 if MessageHandler = 0 then Exit;
521521 PError := StrAlloc(Length(Msg)+1);
522522 StrCopy(PError, PChar(Msg));
523523 PostMessage(formConsole.Handle, WM_VERBOSE, Integer(PError), 0);
524524end;
525525```
526526
527527И пример того, как обработать это сообщение из окна:
528528
529529``` pascal
530530const
531531 WM_GOT_ERROR = LM_USER + 2004;
532532 WM_VERBOSE = LM_USER + 2005;
533533
534534type
535535 { TformConsole }
536536
537537 TformConsole = class(TForm)
538538 DebugList: TListView;
539539 // ...
540540 private
541541 procedure HandleDebug(var Msg: TLMessage); message WM_VERBOSE;
542542 end;
543543
544544var
545545 formConsole: TformConsole;
546546
547547implementation
548548
549549....
550550
551551{ TformConsole }
552552
553553procedure TformConsole.HandleDebug(var Msg: TLMessage);
554554var
555555 Item: TListItem;
556556 MsgStr: PChar;
557557 MsgPasStr: string;
558558begin
559559 MsgStr := PChar(Msg.wparam);
560560 MsgPasStr := StrPas(MsgStr);
561561 Item := DebugList.Items.Add;
562562 Item.Caption := TimeToStr(SysUtils.Now);
563563 Item.SubItems.Add(MsgPasStr);
564564 Item.MakeVisible(False);
565565
566566// Далее следует что-то вроде
567567
568568 TrayControl.SetError(MsgPasStr);
569569 StrDispose(MsgStr)
570570end;
571571
572572end.
573573```
574574
575575Когда на машине Linux x64 возникает ошибка: "Project XY raised exception class 'External: SIGSEGV'", необходимо изменить "Integer(PError)" на "PtrInt(PError)" в процедуре "VerboseLog". Измененная строка должна выглядеть следующим образом:
576576
577577` PostMessage(formConsole.Handle, WM_VERBOSE, PtrInt(PError), 0);`
578578
579579## Критические секции
580580
581581*Критическая секция* представляет собой объект, который используется, чтобы удостовериться, что некоторые части кода выполняются только одним \[конкурирующим\] потоком одновременно. Критическая секция должна быть создана/инициализирована прежде, чем она может быть использована и будет освобождена, когда она не нужна.
582582
583583Критические секции обычно используются следующим образом:
584584
585585Добавляем модуль SyncObjs.
586586
587587Объявляем секцию (для всех потоков, которые будут использовать этот раздел):
588588
589589` MyCriticalSection: TRTLCriticalSection;`
590590
591591Создаём секцию:
592592
593593` InitializeCriticalSection(MyCriticalSection);`
594594
595595Запускаем несколько потоков. Делаем что-нибудь \[в монопольном режиме\]:
596596
597597``` pascal
598598 EnterCriticalSection(MyCriticalSection);
599599 try
600600 // доступ к переменным, запись файлов, отправка сетевых пакетов, и т.д.
601601 finally
602602 LeaveCriticalSection(MyCriticalSection);
603603 end;
604604```
605605
606606После завершения всех потоков, освобождаем её:
607607
608608` DeleteCriticalSection(MyCriticalSection);`
609609
610610В качестве альтернативы вы можете использовать объект TCriticalSection. Создание \[объекта\] выполняет инициализацию, метод Enter выполняет EnterCriticalSection, метод Leave выполняет LeaveCriticalSection, а удаление объекта - \[его\] удаление.
611611
612612Например: 5 потоков инкрементируют счетчик: см. *lazarus/examples/multithreading/criticalsectionexample1.lpi*
613613
614614**Осторожно:** Есть два набора из 4 вышеуказанных функций. По одному из RTL и LCL. \[Для\] LCL \[функции\] определены в модуле LCLIntf и LCLType. Оба \[набора\] работают почти одинаково. Вы можете использовать оба \[набора функций\] в одном приложении одновременно, но не должны использовать функции RTL с критическими секциями LCL и наоборот.
615615
616616### Общие переменные
617617
618618Если неcколько потоков обращаются к переменной, которая предназначена только для чтения, то не о чем беспокоиться. Но если один или несколько потоков изменяют переменную, то необходимо убедиться в том, что в данный момент времени только один поток имеет доступ к ней.
619619
620620Пример: 5 потоков увеличивают переменную-счётчик. см. lazarus/examples/multithreading/criticalsectionexample1.lpi
621621
622622## Ожидание другого потока
623623
624624Если потоку A необходим результат работы другого потока B, он должен ждать пока работа потока B будет завершена.
625625
626626**Важно:** Главный поток никогда не должен ждать другой поток. Вместо этого используйте Synchronize (см. выше).
627627
628628Пример: lazarus/examples/multithreading/waitforexample1.lpi
629629
630630``` pascal
631631{ TThreadA }
632632
633633procedure TThreadA.Execute;
634634begin
635635 Form1.ThreadB:=TThreadB.Create(false);
636636 // создаем событие
637637 WaitForB:=RTLEventCreate;
638638 while not Application.Terminated do begin
639639 // ждем бесконечно (пока B не разбудит A)
640640 RtlEventWaitFor(WaitForB);
641641 writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter));
642642 end;
643643end;
644644
645645{ TThreadB }
646646
647647procedure TThreadB.Execute;
648648var
649649 i: Integer;
650650begin
651651 Counter:=0;
652652 while not Application.Terminated do begin
653653 // B: Работает ...
654654 Sleep(1500);
655655 inc(Counter);
656656 // будим A
657657 RtlEventSetEvent(Form1.ThreadA.WaitForB);
658658 end;
659659end;
660660```
661661
662662Примечание: RtlEventSetEvent может быть вызвана перед RtlEventWaitFor. Тогда RtlEventWaitFor будет немедленно завершён (return). Используйте RTLeventResetEvent чтобы очистить флаг.
663663
664664## Fork (порождение)
665665
666666При порождении в многопоточном приложении, следует знать, что любые потоки, созданные и запущенные ПЕРЕД вызовом fork (или fpFork), НЕ будут запущены в дочернем процессе. Как заявлено в справочной странице fork(), невозможно определить состояние любых потоков, которые были запущены перед вызовом fork().
667667
668668Если есть какие-либо потоки, инициализированные перед вызовом (включая секцию инициализации), они не будут работать.
669669
670670## Параллельные процедуры/циклы
671671
672672Особый случай работы много поточности во время выполнения кода смотрите [[Parallel procedures/ru|здесь]].
673673
674674## Распределённые вычисления
675675
676676Следующим шагом после многопоточности является запуск потоков на различных машинах.
677677
678678- Вы можете использовать один из TCP компонентов, таких как synapse, lnet или indy для связи. Это даст Вам наибольшую гибкость и, в основном, используется для слабо связанных Клиент / Серверных приложений.
679679- Вы можете использовать библиотеки передач сообщений, таких как [[MPICH]], которые используются для высокопроизводительных вычислений (HPC, High Performance Computing) в кластерах.
680680
681681## Внешние потоки
682682
683683Чтобы система потоков Free Pascal работала должным образом, каждый вновь созданный поток FPC должен быть инициализирован (точнее, исключение, система ввода-вывода и система threadvar для каждого потока должны быть инициализированы для работы потоков и кучи). Для вас это делается полностью автоматически, если вы используете BeginThread (или косвенно, используя класс TThread). Однако если вы используете потоки, которые были созданы без BeginThread (то есть внешние потоки), может потребоваться дополнительная работа (в настоящее время). Внешние потоки также включают те, которые были созданы во внешних библиотеках Cи (.DLL/.so).
684684
685685Что следует учитывать при использовании внешних потоков (возможно не понадобится во всех или будущих версиях компилятора):
686686
687687- Не используйте внешние потоки вообще - используйте потоки FPC. Если вы можете получить контроль над тем, как создается поток, создайте его самостоятельно с помощью BeginThread.
688688
689689Если соглашение о вызовах не подходит (например, если вашей исходной функции потока требуется соглашение о вызовах cdecl, но BeginThread требует соглашения Паскаля), создайте запись, сохраните в ней исходную требуемую функцию потока и вызовите эту функцию в вашей паскалевской функции потока:
690690
691691``` pascal
692692type
693693 TCdeclThreadFunc = function (user_data:Pointer):Pointer;cdecl;
694694
695695 PCdeclThreadFuncData = ^TCdeclThreadFuncData;
696696 TCdeclThreadFuncData = record
697697 Func: TCdeclThreadFunc; //функция cdecl
698698 Data: Pointer; //исходные данные
699699 end;
700700
701701// Паскалевский поток вызывает функцию cdecl
702702function C2P_Translator(FuncData: pointer) : ptrint;
703703var
704704 ThreadData: TCdeclThreadFuncData;
705705begin
706706 ThreadData := PCdeclThreadFuncData(FuncData)^;
707707 Result := ptrint(ThreadData.Func(ThreadData.Data));
708708end;
709709
710710procedure CreatePascalThread;
711711var
712712 ThreadData: PCdeclThreadFuncData;
713713begin
714714 New(ThreadData);
715715 // это желаемая функция потока cdecl
716716 ThreadData^.Func := func;
717717 ThreadData^.Data := user_data;
718718 // это создаст поток Паскаля
719719 BeginThread(@C2P_Translator, ThreadData );
720720end;
721721```
722722
723723- Инициализируйте систему потоков FPC, создав фиктивный поток. Если вы не создадите поток Pascal в своем приложении, система потоков не будет инициализирована (и, следовательно, потоки не будут работать и, следовательно, куча не будет работать правильно).
724724
725725``` pascal
726726type
727727 tc = class(tthread)
728728 procedure execute;override;
729729 end;
730730
731731 procedure tc.execute;
732732 begin
733733 end;
734734
735735{ main program }
736736begin
737737 { инициализация системы потоков}
738738 with tc.create(false) do
739739 begin
740740 waitfor;
741741 free;
742742 end;
743743 { ... ваш последующий код }
744744end.
745745```
746746
747747> **Note:** После инициализации системы потоков среда выполнения может установить для системной переменной IsMultiThread значение true, которое используется подпрограммами FPC для выполнения блокировок здесь и там. Вы не должны устанавливать значение этой переменной вручную.
748748
749749- Если по какой-то причине у вас это не работает, попробуйте этот код в функции внешнего потока:
750750
751751``` pascal
752752function ExternalThread(param: Pointer): LongInt; stdcall;
753753var
754754 tm: TThreadManager;
755755begin
756756 GetThreadManager(tm);
757757 tm.AllocateThreadVars;
758758 InitThread(1000000); // отрегулируйте начальный размер стека здесь
759759
760760 { делаем что-то в потоке здесь ... }
761761
762762 Result:=0;
763763end;
764764```
765765
766766### Идентификация внешних потоков
767767
768768Иногда вы даже не знаете, приходится ли вам иметь дело с внешними потоками (например, если какая-то библиотека Cи выполняет обратный вызов). Этот алгоритм может помочь понять это:
769769
7707701\. Спрашиваем у ОС идентификатор текущего потока при запуске вашего приложения
771771
772772``` pascal
773773GetCurrentThreadID() //windows;
774774GetThreadID() //Darwin/macOS;
775775TThreadID(pthread_self) //Linux;
776776```
777777
7787782\. Спрашиваем еще раз идентификатор текущего потока внутри функции потока и сравниваем его с результатом шага 1.
779779
780780### Откажитесь от использования временных функций
781781
782782``` pascal
783783ThreadSwitch()
784784```
785785
786786> **Note:** Не используйте виндовый трюк со [*Sleep(0)*](https://www.freepascal.org/docs-html/rtl/sysutils/sleep.html), так как это будет работать не на всех платформах.
787787
788788## См. также
789789
790790- [[Streaming components|Компоненты потоковой (stream) передачи данных]]
791791- [[Manager Worker Threads System]]
792792- [[Example of multi-threaded application: array of threads]]
793793- [[Parallel procedures]]
794794- [[Main Loop Hooks]]
795795- [[Asynchronous Calls]]
796796- [Mutlithreading - The Delphi way](http://nickhodges.com/MultiThreadingInDelphi/ToC.html)
797797
798798---
799799*From the Free Pascal wiki, page "Multithreaded Application Tutorial/ru", as it stood on 2022-01-27 after a change by Alextp, 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