Sign in Join
Wiki copytemporary

Multithreaded Application Tutorial/ru: what changed

Revision 2 to 3. 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
48-` This binary has no thread support compiled in.`\
49-` Recompile the application with a thread-driver in the program uses clause before other units using thread.`\
50-` { В этом двоичном файле нет поддержки потоков.}`\
51-` {Перекомпилируйте приложение с драйвером потока, указанном в разделе uses программы перед другими модулями, использующими поток.}`
48+``` pascal
49+ This binary has no thread support compiled in.
50+ Recompile the application with a thread-driver in the program uses clause before other units using thread.
51+ { В этом двоичном файле нет поддержки потоков.}
52+ {Перекомпилируйте приложение с драйвером потока, указанном в разделе uses программы перед другими модулями, использующими поток.}
53+```
5254
5355> **Note:** Если вы получаете ошибку компоновщика о том, что "mcount" не найден, значит, вы используете какой-то модуль, содержащий многопоточный код, и вам нужно добавить модуль `cthreads` или использовать интеллектуальное связывание.
5456
5557> **Note:** Если вы получаете ошибку: "Project raised exception class 'RunError(232)'" в процедуре SYSTEM_NOTHREADERROR, то ваш код требует многопоточности и вам необходимо добавить модуль cthreads.
5658
5759## Простейший пример FPC
5860
5961Приведенный ниже код дает очень простой пример. Проверено с помощью FPC 3.0.4 на Win7.
6062
6163``` pascal
6264Program ThreadTest;
6365{тестирование возможности многопоточности}
6466{
6567 OUTPUT
6668thread 1 started
6769thread 1 thri 0 Len(S)= 1
6870thread 1 thri 1 Len(S)= 2
6971thread 1 thri 2 Len(S)= 3
7072thread 1 thri 3 Len(S)= 4
7173thread 1 thri 4 Len(S)= 5
7274thread 1 thri 5 Len(S)= 6
7375thread 1 thri 6 Len(S)= 7
7476thread 1 thri 7 Len(S)= 8
7577thread 1 thri 8 Len(S)= 9
7678thread 1 thri 9 Len(S)= 10
7779thread 1 thri 10 Len(S)= 11
7880thread 1 thri 11 Len(S)= 12
7981thread 1 thri 12 Len(S)= 13
8082thread 1 thri 13 Len(S)= 14
8183thread 1 thri 14 Len(S)= 15
8284thread 2 started
8385thread 3 started
8486thread 1 thri 15 Len(S)= 16
8587thread 2 thri 0 Len(S)= 1
8688thread 3 thri 0 Len(S)= 1
8789thread 1 thri 16 Len(S)= 17
8890...
8991...
9092thread 5 thri 997 Len(S)= 998
9193thread 5 thri 998 Len(S)= 999
9294thread 5 thri 999 Len(S)= 1000
9395thread 5 finished
9496thread 10 thri 828 Len(S)= 829
9597thread 9 thri 675 Len(S)= 676
9698thread 4 thri 656 Len(S)= 657
9799thread 10 thri 829 Len(S)= 830
98100thread 9 thri 676 Len(S)= 677
99101thread 9 thri 677 Len(S)= 678
100102thread 10 thri 830 Len(S)= 831
101103thread 10 thri 831 Len(S)= 832
102104thread 10 thri 832 Len(S)= 833
103105thread 10 thri 833 Len(S)= 834
104106thread 10 thri 834 Len(S)= 835
105107thread 10 thri 835 Len(S)= 836
106108thread 10 thri 836 Len(S)= 837
107109thread 10 thri 837 Len(S)= 838
108110thread 10 thri 838 Len(S)= 839
109111thread 10 thri 839 Len(S)= 840
110112thread 9 thri 678 Len(S)= 679
111113...
112114...
113115thread 4 thri 994 Len(S)= 995
114116thread 4 thri 995 Len(S)= 996
115117thread 4 thri 996 Len(S)= 997
116118thread 4 thri 997 Len(S)= 998
117119thread 4 thri 998 Len(S)= 999
118120thread 4 thri 999 Len(S)= 1000
119121thread 4 finished
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('thread ',longint(p),' started');
142144 thri:=0;
143145 while (thri<stringlen) do begin
144146 s:=s+'1'; { create a delay }
145147 writeln('thread ',longint(p),' thri ',thri,' Len(S)= ',length(s));
146148 inc(thri);
147149 end;
148150 Writeln('thread ',longint(p),' finished');
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## Класс TThread
166168
167169Самый простой путь для создания многопоточного приложения — использование **класса TThread**. Этот класс позволяет достаточно просто создать дополнительный поток (наряду с основным). Обычно от вас требуется переопределить только 2 метода: **конструктор Create**, и **метод Execute**.
168170
169171В **конструкторе** Вы подготавливаете поток для запуска. Вы устанавливаете начальные значения переменных и свойств, которые Вам нужны. Подлинный конструктор TThread требует параметр, называющийся **Suspended** (приостановить). Как и следует ожидать, установка Suspended = True будет препятствовать автоматическому запуску потоков после создания. Если Suspended = False, поток запускается сразу после создания. Если поток создан приостановленным (Suspended = True), тогда он запускается только после вызова **метода Resume**.
170172
171173В FPC version 2.0.1 и более поздних, TThread.Create также имеет неявный параметр, отвечающий за размер стека. Теперь, если это необходимо, Вы можете изменить стандартный размер стека каждого потока, который Вы создали. Глубокая рекурсия в процедурах является хорошим примером. Если Вы не изменяете размер стека, используется стандартный размер стека операционной системы.
172174
173175В переопределённом **методе Execute** Вы будете писать код, который запускается в потоке.
174176
175177Класс TThread имеет одно важное свойство: **Terminated** : Boolean;
176178
177179Если поток имеет цикл (loop) — и это нормально, — выход из цикла должен осуществиться, когда **Terminated = True** (по умолчанию – False). В каждом проходе значение свойство Terminated должно проверяться, и если оно True, цикл должен завершиться так быстро, как это возможно, после необходимой очистки. Имейте ввиду, что метод **Terminate** по умолчанию ничего не делает: в метод **Execute** нужно явно организовать завершение задания.
178180
179181Как мы объяснили ранее, поток не должен взаимодействовать с видимыми компонентами. Обновления для видимой компоненты должны быть сделаны в контексте главного, основного потока. Чтобы это сделать, у TThread существует метод, называющийся **Synchronize**. Synchronize требует метод в качестве аргумента. Когда вы вызываете метод через **Synchronize(@MyMethod)**, запущенный поток приостанавливается, код метода **MyMethod** запускается в главном потоке, а затем выполнение потока будет продолжено. Точная обработка Synchronize зависит от платформы, но упрощённо он делает следующее: он посылает сообщение в главную очередь сообщений и приостанавливает своё выполнение(goes to sleep). В конечном итоге главный поток обрабатывает сообщение и вызывает **MyMethod**. Таким образом, **MyMethod** вызывается без контекста, то есть не во время события **OnMouseDown** или в течение события перерисовки (paint event), но после этого. После того, как главный поток запускает **MyMethod**, он будит спящий поток и обрабатывает следующее сообщение из главной очереди сообщений, а поток продолжает работать.
180182
181183Существует другое важное свойство TThread: **FreeOnTerminate**. Если оно True, тогда объект потока автоматически освобождается, когда выполнение потока (метод **Execute**) останавливается. В противном случае, приложению будет необходимо освободить его вручную.
182184
183185Следующий пример может быть найден в каталоге *examples/multithreading/*:
184186
185187``` pascal
186188 Type
187189 TMyThread = class(TThread)
188190 private
189191 fStatusText : string;
190192 procedure ShowStatus;
191193 protected
192194 procedure Execute; override;
193195 public
194196 Constructor Create(CreateSuspended : boolean);
195197 end;
196198
197199 constructor TMyThread.Create(CreateSuspended : boolean);
198200 begin
199201 FreeOnTerminate := True;
200202 inherited Create(CreateSuspended);
201203 end;
202204
203205 procedure TMyThread.ShowStatus;
204206 // этот метод запущен главным потоком и поэтому может получить доступ ко всем элементам графического интерфейса.
205207 begin
206208 Form1.Caption := fStatusText;
207209 end;
208210
209211 procedure TMyThread.Execute;
210212 var
211213 newStatus : string;
212214 begin
213215 fStatusText := 'TMyThread Starting...';
214216 Synchronize(@Showstatus);
215217 fStatusText := 'TMyThread Running...';
216218 while (not Terminated) and ([любое необходимое условие]) do
217219 begin
218220 ...
219221 {здесь расположен код цикла главного (основного) потока}
220222 ...
221223 if NewStatus <> fStatusText then
222224 begin
223225 fStatusText := newStatus;
224226 Synchronize(@Showstatus);
225227 end;
226228 end;
227229 end;
228230```
229231
230232В приложении
231233
232234``` pascal
233235 var
234236 MyThread : TMyThread;
235237 begin
236238 MyThread := TMyThread.Create(True); // Таким способом он не запустится автоматически
237239 ...
238240 {Здесь расположен код, который инициализирует всё возможное перед запуском потоков}
239241 ...
240242 MyThread.Resume;
241243 end;
242244```
243245
244246Если вы хотите, чтобы ваши приложения были более гибкими, Вы можете создавать события для потока; таким образом, Ваши синхронизированные методы не будут тесно связаны с определенной формой или классом: Вы можете прикрепить слушателей событий потока. Вот пример:
245247
246248``` pascal
247249 Type
248250 TShowStatusEvent = procedure(Status: String) of Object;
249251
250252 TMyThread = class(TThread)
251253 private
252254 fStatusText : string;
253255 FOnShowStatus: TShowStatusEvent;
254256 procedure ShowStatus;
255257 protected
256258 procedure Execute; override;
257259 public
258260 Constructor Create(CreateSuspended : boolean);
259261 property OnShowStatus: TShowStatusEvent read FOnShowStatus write FOnShowStatus;
260262 end;
261263
262264 constructor TMyThread.Create(CreateSuspended : boolean);
263265 begin
264266 FreeOnTerminate := True;
265267 inherited Create(CreateSuspended);
266268 end;
267269
268270 procedure TMyThread.ShowStatus;
269271 // этот метод запущен главным потоком и поэтому может получить доступ ко всем элементам графического интерфейса.
270272 begin
271273 if Assigned(FOnShowStatus) then
272274 begin
273275 FOnShowStatus(fStatusText);
274276 end;
275277 end;
276278
277279 procedure TMyThread.Execute;
278280 var
279281 newStatus : string;
280282 begin
281283 fStatusText := 'TMyThread Starting...';
282284 Synchronize(@Showstatus);
283285 fStatusText := 'TMyThread Running...';
284286 while (not Terminated) and ([любое необходимое условие]) do
285287 begin
286288 ...
287289 {здесь расположен код цикла главного (основного) потока}
288290 ...
289291 if NewStatus <> fStatusText then
290292 begin
291293 fStatusText := newStatus;
292294 Synchronize(@Showstatus);
293295 end;
294296 end;
295297 end;
296298```
297299
298300В приложении
299301
300302``` pascal
301303 Type
302304 TForm1 = class(TForm)
303305 Button1: TButton;
304306 Label1: TLabel;
305307 procedure FormCreate(Sender: TObject);
306308 procedure FormDestroy(Sender: TObject);
307309 private
308310 { private declarations }
309311 MyThread: TMyThread;
310312 procedure ShowStatus(Status: string);
311313 public
312314 { public declarations }
313315 end;
314316
315317 procedure TForm1.FormCreate(Sender: TObject);
316318 begin
317319 inherited;
318320 MyThread := TMyThread.Create(true);
319321 MyThread.OnShowStatus := @ShowStatus;
320322 end;
321323
322324 procedure TForm1.FormDestroy(Sender: TObject);
323325 begin
324326 MyThread.Terminate;
325327 MyThread.Free;
326328 inherited;
327329 end;
328330
329331 procedure TForm1.Button1Click(Sender: TObject);
330332 begin
331333 MyThread.Resume;
332334 end;
333335
334336 procedure TForm1.ShowStatus(Status: string);
335337 begin
336338 Label1.Caption := Status;
337339 end;
338340```
339341
340342## Важно знать
341343
342344### Проверка стека в Windows
343345
344346Существует потенциальная проблема с потоками в Windows, если Вы используете переключатель (?) -Ct (stack check). По причинам не совсем понятным проверка стека "переключается" при любом TThread.Create если Вы используете размер стек по-умолчанию.
345347
346348Единственный работающий способ на данный момент - просто не использовать переключатель -Ct. Заметим, что это не вызывает исключения в основном потоке, но вызывает в недавно созданном. Это "выглядит" как если бы поток никогда не был запущен.
347349
348350Хороший код для проверки этого и других исключений, которые могут возникнуть в создании потоков:
349351
350352``` pascal
351353 MyThread:=TThread.Create(False);
352354 if Assigned(MyThread.FatalException) then
353355 raise MyThread.FatalException;
354356```
355357
356358Этот код будет гарантировать, что любое исключение, которое произошло во время создания потока будет распространёно на главный поток.
357359
358360### Функция GetDC на Windows
359361
360362Функция GetDC (и GetDCEx) извлекает хэндл контекста устройства (DC) для клиентской области указанного окна или для всего экрана. Ее можно использовать, например, для [[Developing with Graphics|создания снимка экрана]]. Однако обратите внимание, что эта функция не является потокобезопасной: хэндл к DC может использоваться только одним потоком в любой момент времени, как [описано здесь](https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getdc).
361363
362364### Многопоточность в пакетах
363365
364366Пакеты, использующие многопоточность, должны добавлять флаг **-dUseCThreads** в пользовательские параметры использования. Откройте редактор пакета, затем Options \> Usage \> Custom и добавьте *-dUseCThreads*. Это определит данный флаг для всех проектов и пакетов, использующих этот пакет, включая IDE. IDE и все новые приложения, созданные IDE, уже имеют следующий код в своем .lpr-файле:
365367
366368``` pascal
367369uses
368370 {$IFDEF UNIX}{$IFDEF UseCThreads}
369371 cthreads,
370372 cmem, // менеджер памяти Си в некоторых системах намного быстрее для многопоточности
371373 {$ENDIF}{$ENDIF}
372374```
373375
374376### Heaptrc
375377
376378Вы не можете использовать ключ -gh с модулем *cmem*. Ключ -gh использует модуль heaptrc, который расширяет менеджер кучи. Поэтому модуль **heaptrc** должен указываться **после** модуля **cmem**.
377379
378380``` pascal
379381uses
380382 {$IFDEF UNIX}{$IFDEF UseCThreads}
381383 cthreads,
382384 cmem, // менеджер памяти Си в некоторых системах намного быстрее для многопоточности
383385 {$ENDIF}{$ENDIF}
384386 heaptrc,
385387```
386388
387389### Initialization и Finalization
388390
389391Чтобы инициализировать сам объект потока, вы можете либо запустить его в приостановленном состоянии и задать его свойства и/или создать новый конструктор и вызвать унаследованный конструктор.
390392
391393**Примечание**: Использование AfterConstruction при `CreateSuspended=false` опасно, так как поток уже запущен.
392394
393395С другой стороны, деструктор может быть использован для завершения использования ресурсов объекта.
394396
395397``` pascal
396398type
397399 TMyThread = class(TThread)
398400 private
399401 fRTLEvent: PRTLEvent;
400402 public
401403 procedure Create(SomeData: TSomeObject); override;
402404 destructor Destroy; override;
403405 end;
404406
405407procedure TMyThread.Create(SomeData: TSomeObject; CreateSuspended: boolean);
406408begin
407409 // например: настройка событий, критических секций и других ресурсов, таких как файлы или соединения с базой данных
408410 RTLEventCreate(fRTLEvent);
409411 inherited Create(CreateSuspended);
410412end;
411413
412414destructor TMyThread.Destroy;
413415begin
414416 RTLeventDestroy(fRTLEvent);
415417 inherited Destroy;
416418end;
417419```
418420
419421### Не LCL-программы
420422
421423TThread.Synchronize требует, чтобы главный поток регулярно вызывал CheckSynchronize. LCL делает это в своем цикле. Если вы не используете цикл событий LCL, вы должны вызывать его самостоятельно.
422424
423425## Поддержка SMP
424426
425427Хорошей новостью является то, что если ваше приложение работает все-таки правильно в многопоточном режиме, то оно уже поддерживает SMP!
426428
427429## Отладка многопоточных приложений с помощью Lazarus
428430
429431Отладка на Lazarus требует GDB и быстро становится все более полнофункциональной и стабильной. Однако до сих пор существует несколько дистрибутивов Linux с некоторыми проблемами.
430432
431433### Вывод отладочной информации
432434
433435В однопоточном приложении вы можете просто писать в консоль/терминал/что угодно, и порядок строк будет таким же, как они были записаны. В многопоточном приложении все сложнее. Если пишут два потока, скажем, строка написана потоком A перед строкой потока B, то строки не обязательно будут записаны в таком порядке. В то время как в linux (возможно) вы получите правильный вывод [[DebugLn]](), то в win32 вы можете получить исключения (возможно DiskFull) из-за использования DebugLn() вне основного потока. Поэтому, чтобы избежать головной боли, используйте DebugLnThreadLog(), упомянутый ниже.
434436
435437Модуль LCLProc содержит несколько функций, позволяющих каждому потоку писать в свой собственный файл журнала:
436438
437439``` pascal
438440 procedure DbgOutThreadLog(const Msg: string); overload;
439441 procedure DebuglnThreadLog(const Msg: string); overload;
440442 procedure DebuglnThreadLog(Args: array of const); overload;
441443 procedure DebuglnThreadLog; overload;
442444```
443445
444446Например: Вместо *writeln('Some text ',123);* задействуйте
445447
446-` DebuglnThreadLog(['Some text ',123]);`
448+```text
449+ DebuglnThreadLog(['Some text ',123]);
450+```
447451
448452Это добавит строку 'Some text 123' в **Log\<PID\>.txt**, где \<PID\> - идентификатор процесса текущего потока.
449453
450454Хорошей идеей будет удаление файлов журнала перед каждым запуском:
451455
452-` rm -f Log* && ./project1`
456+```text
457+ rm -f Log* && ./project1
458+```
453459
454460### Linux
455461
456462Если вы попытаетесь отладить многопоточное приложение в Linux, вы столкнетесь с одной большой проблемой: менеджер рабочего стола на X-сервере может зависнуть. Это происходит, например, когда приложение захватило мышь/клавиатуру и было приостановлено gdb, а X-сервер ждет вашего приложения. Когда это происходит, вы можете просто войти в систему с другого компьютера и убить gdb или выйти из этой сессии, нажав `CTRL`+`ALT`+`F3` и убить gdb. В качестве альтернативы вы можете перезапустить оконный менеджер: введите
457463
458-` sudo /etc/init.d/gdm restart. `
464+```text
465+ sudo /etc/init.d/gdm restart.
466+```
459467
460468Это перезапустит менеджер рабочего стола и вернет вас на рабочий стол.
461469
462470Поскольку это зависит от того, где gdb останавливает вашу программу, в некоторых случаях могут помочь некоторые хитрости: для Ubuntu x64 установите в опциях проекта для отладки требуемый файл дополнительной информации...
463471
464-` Project Options -> Compiler Options -> Linking -> Debugging: Check Use external gdb debug symbols file (-Xg).`
472+```text
473+ Project Options -> Compiler Options -> Linking -> Debugging: Check Use external gdb debug symbols file (-Xg).
474+```
465475
466476Другой вариант - открыть другой рабочий стол X, запустить IDE/gdb на одном и приложение на другом, чтобы зависал только тестовый рабочий стол. Создайте новый экземпляр X с:
467477
468-` X :1 &`
478+```text
479+ X :1 &
480+```
469481
470482Он откроется, и когда вы переключитесь на другой рабочий стол (тот, с которым вы работаете, нажав `CTRL`+`ALT`+`F7`), вы сможете вернуться на новый графический рабочий стол с помощью `CTRL`+`ALT`+`F8` (если эта комбинация не работает, попробуйте `CTRL`+`ALT`+`F2`... этот вариант работал на [Slackware](http://www.slackware.com)).
471483
472484Затем вы можете, если хотите, создать сеанс рабочего стола на X, с которого начался сеанс:
473485
474-` gnome-session --display=:1 &`
486+```text
487+ gnome-session --display=:1 &
488+```
475489
476490Затем в Lazarus, в диалоге параметров запуска проекта, отметьте "Use display" и введите :1.
477491
478492Теперь приложение будет запущено на втором X-сервере, и вы сможете отлаживать его на первом.
479493
480494Это было проверено с Free Pascal 2.0 и Lazarus 0.9.10 на Windows и Linux.
481495
482496------------------------------------------------------------------------
483497
484498Вместо создания новой X-сессии можно использовать [Xnest](http://en.wikipedia.org/wiki/Xnest). Xnest - это X-сессия в окне. Используя его, X-сервер не блокируется при отладке потоков, и отлаживать гораздо проще, не меняя терминалы.
485499
486500Командная строка для запуска Xnest выглядит следующим образом
487501
488-` Xnest :1 -ac`
502+```text
503+ Xnest :1 -ac
504+```
489505
490506для создания X-сессии на :1 и отключения контроля доступа.
491507
492508## Интерфейсы виджетов Lazarus
493509
494510Интерфейсы win32, gtk и carbon поддерживают многопоточность. Это означает, что TThread, критические секции и Synchronize работают. Но они не являются потокобезопасными. Это означает, что только один поток в одно время может получить доступ к LCL. А поскольку главный поток никогда не должен ждать другого потока, это означает, что только главный поток имеет доступ к LCL, то есть ко всему, что связано с TControl, Application и дескрипторами виджетов LCL. В LCL есть несколько потокобезопасных функций. Например, большинство функций в блоке FileUtil являются потокобезопасными.
495511
496512### Использование SendMessage/PostMessage для взаимодействия между потоками
497513
498514Только один поток в приложении должен вызывать API LCL, обычно это главный поток. Другие потоки могут использовать LCL с помощью ряда косвенных методов, одним из хороших вариантов является использование SendMessage или PostMessage. LCLIntf.SendMessage и LCLIntf.PostMessage отправляют сообщение в окно пула сообщений приложения.
499515
500516См. также документацию по этим процедурам:
501517
502518- [[doc:lcl/lclintf/sendmessage.html|SendMessage]]
503519- [[doc:lcl/lclintf/postmessage.html|PostMessage]]
504520
505521Разница между SendMessage и PostMessage заключается в том, как они возвращают управление вызывающему потоку. Как и Synchronize, SendMessage блокирует и управление не возвращается, пока окно, в которое было отправлено сообщение, не завершит его обработку; однако при определенных обстоятельствах SendMessage может попытаться оптимизировать обработку, оставаясь в контексте вызвавшего его потока. При PostMessage управление возвращается немедленно до некоторого определенного системой максимального количества поставленных в очередь сообщений и до тех пор, пока в куче остается место для вложенных данных.
506522
507523В обоих случаях процедура, обрабатывающая сообщение (см. ниже), должна избегать вызова application.ProcessMessages, поскольку это может привести к отправке второго сообщения, которое будет обрабатываться реентерабельно. Если это неизбежно, то предпочтительнее использовать другой механизм для передачи сериализованных событий между потоками.
508524
509525Вот пример того, как вторичный поток может посылать текст для отображения в элементе управления LCL главному потоку:
510526
511527``` pascal
512528const
513529 WM_GOT_ERROR = LM_USER + 2004;
514530 WM_VERBOSE = LM_USER + 2005;
515531
516532procedure VerboseLog(Msg: string);
517533var
518534 PError: PChar;
519535begin
520536 if MessageHandler = 0 then Exit;
521537 PError := StrAlloc(Length(Msg)+1);
522538 StrCopy(PError, PChar(Msg));
523539 PostMessage(formConsole.Handle, WM_VERBOSE, Integer(PError), 0);
524540end;
525541```
526542
527543И пример того, как обработать это сообщение из окна:
528544
529545``` pascal
530546const
531547 WM_GOT_ERROR = LM_USER + 2004;
532548 WM_VERBOSE = LM_USER + 2005;
533549
534550type
535551 { TformConsole }
536552
537553 TformConsole = class(TForm)
538554 DebugList: TListView;
539555 // ...
540556 private
541557 procedure HandleDebug(var Msg: TLMessage); message WM_VERBOSE;
542558 end;
543559
544560var
545561 formConsole: TformConsole;
546562
547563implementation
548564
549565....
550566
551567{ TformConsole }
552568
553569procedure TformConsole.HandleDebug(var Msg: TLMessage);
554570var
555571 Item: TListItem;
556572 MsgStr: PChar;
557573 MsgPasStr: string;
558574begin
559575 MsgStr := PChar(Msg.wparam);
560576 MsgPasStr := StrPas(MsgStr);
561577 Item := DebugList.Items.Add;
562578 Item.Caption := TimeToStr(SysUtils.Now);
563579 Item.SubItems.Add(MsgPasStr);
564580 Item.MakeVisible(False);
565581
566582// Далее следует что-то вроде
567583
568584 TrayControl.SetError(MsgPasStr);
569585 StrDispose(MsgStr)
570586end;
571587
572588end.
573589```
574590
575591Когда на машине Linux x64 возникает ошибка: "Project XY raised exception class 'External: SIGSEGV'", необходимо изменить "Integer(PError)" на "PtrInt(PError)" в процедуре "VerboseLog". Измененная строка должна выглядеть следующим образом:
576592
577-` PostMessage(formConsole.Handle, WM_VERBOSE, PtrInt(PError), 0);`
593+```text
594+ PostMessage(formConsole.Handle, WM_VERBOSE, PtrInt(PError), 0);
595+```
578596
579597## Критические секции
580598
581599*Критическая секция* представляет собой объект, который используется, чтобы удостовериться, что некоторые части кода выполняются только одним \[конкурирующим\] потоком одновременно. Критическая секция должна быть создана/инициализирована прежде, чем она может быть использована и будет освобождена, когда она не нужна.
582600
583601Критические секции обычно используются следующим образом:
584602
585603Добавляем модуль SyncObjs.
586604
587605Объявляем секцию (для всех потоков, которые будут использовать этот раздел):
588606
589-` MyCriticalSection: TRTLCriticalSection;`
607+```text
608+ MyCriticalSection: TRTLCriticalSection;
609+```
590610
591611Создаём секцию:
592612
593-` InitializeCriticalSection(MyCriticalSection);`
613+```text
614+ InitializeCriticalSection(MyCriticalSection);
615+```
594616
595617Запускаем несколько потоков. Делаем что-нибудь \[в монопольном режиме\]:
596618
597619``` pascal
598620 EnterCriticalSection(MyCriticalSection);
599621 try
600622 // доступ к переменным, запись файлов, отправка сетевых пакетов, и т.д.
601623 finally
602624 LeaveCriticalSection(MyCriticalSection);
603625 end;
604626```
605627
606628После завершения всех потоков, освобождаем её:
607629
608-` DeleteCriticalSection(MyCriticalSection);`
630+```text
631+ DeleteCriticalSection(MyCriticalSection);
632+```
609633
610634В качестве альтернативы вы можете использовать объект TCriticalSection. Создание \[объекта\] выполняет инициализацию, метод Enter выполняет EnterCriticalSection, метод Leave выполняет LeaveCriticalSection, а удаление объекта - \[его\] удаление.
611635
612636Например: 5 потоков инкрементируют счетчик: см. *lazarus/examples/multithreading/criticalsectionexample1.lpi*
613637
614638**Осторожно:** Есть два набора из 4 вышеуказанных функций. По одному из RTL и LCL. \[Для\] LCL \[функции\] определены в модуле LCLIntf и LCLType. Оба \[набора\] работают почти одинаково. Вы можете использовать оба \[набора функций\] в одном приложении одновременно, но не должны использовать функции RTL с критическими секциями LCL и наоборот.
615639
616640### Общие переменные
617641
618642Если неcколько потоков обращаются к переменной, которая предназначена только для чтения, то не о чем беспокоиться. Но если один или несколько потоков изменяют переменную, то необходимо убедиться в том, что в данный момент времени только один поток имеет доступ к ней.
619643
620644Пример: 5 потоков увеличивают переменную-счётчик. см. lazarus/examples/multithreading/criticalsectionexample1.lpi
621645
622646## Ожидание другого потока
623647
624648Если потоку A необходим результат работы другого потока B, он должен ждать пока работа потока B будет завершена.
625649
626650**Важно:** Главный поток никогда не должен ждать другой поток. Вместо этого используйте Synchronize (см. выше).
627651
628652Пример: lazarus/examples/multithreading/waitforexample1.lpi
629653
630654``` pascal
631655{ TThreadA }
632656
633657procedure TThreadA.Execute;
634658begin
635659 Form1.ThreadB:=TThreadB.Create(false);
636660 // создаем событие
637661 WaitForB:=RTLEventCreate;
638662 while not Application.Terminated do begin
639663 // ждем бесконечно (пока B не разбудит A)
640664 RtlEventWaitFor(WaitForB);
641665 writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter));
642666 end;
643667end;
644668
645669{ TThreadB }
646670
647671procedure TThreadB.Execute;
648672var
649673 i: Integer;
650674begin
651675 Counter:=0;
652676 while not Application.Terminated do begin
653677 // B: Работает ...
654678 Sleep(1500);
655679 inc(Counter);
656680 // будим A
657681 RtlEventSetEvent(Form1.ThreadA.WaitForB);
658682 end;
659683end;
660684```
661685
662686Примечание: RtlEventSetEvent может быть вызвана перед RtlEventWaitFor. Тогда RtlEventWaitFor будет немедленно завершён (return). Используйте RTLeventResetEvent чтобы очистить флаг.
663687
664688## Fork (порождение)
665689
666690При порождении в многопоточном приложении, следует знать, что любые потоки, созданные и запущенные ПЕРЕД вызовом fork (или fpFork), НЕ будут запущены в дочернем процессе. Как заявлено в справочной странице fork(), невозможно определить состояние любых потоков, которые были запущены перед вызовом fork().
667691
668692Если есть какие-либо потоки, инициализированные перед вызовом (включая секцию инициализации), они не будут работать.
669693
670694## Параллельные процедуры/циклы
671695
672696Особый случай работы много поточности во время выполнения кода смотрите [[Parallel procedures/ru|здесь]].
673697
674698## Распределённые вычисления
675699
676700Следующим шагом после многопоточности является запуск потоков на различных машинах.
677701
678702- Вы можете использовать один из TCP компонентов, таких как synapse, lnet или indy для связи. Это даст Вам наибольшую гибкость и, в основном, используется для слабо связанных Клиент / Серверных приложений.
679703- Вы можете использовать библиотеки передач сообщений, таких как [[MPICH]], которые используются для высокопроизводительных вычислений (HPC, High Performance Computing) в кластерах.
680704
681705## Внешние потоки
682706
683707Чтобы система потоков Free Pascal работала должным образом, каждый вновь созданный поток FPC должен быть инициализирован (точнее, исключение, система ввода-вывода и система threadvar для каждого потока должны быть инициализированы для работы потоков и кучи). Для вас это делается полностью автоматически, если вы используете BeginThread (или косвенно, используя класс TThread). Однако если вы используете потоки, которые были созданы без BeginThread (то есть внешние потоки), может потребоваться дополнительная работа (в настоящее время). Внешние потоки также включают те, которые были созданы во внешних библиотеках Cи (.DLL/.so).
684708
685709Что следует учитывать при использовании внешних потоков (возможно не понадобится во всех или будущих версиях компилятора):
686710
687711- Не используйте внешние потоки вообще - используйте потоки FPC. Если вы можете получить контроль над тем, как создается поток, создайте его самостоятельно с помощью BeginThread.
688712
689713Если соглашение о вызовах не подходит (например, если вашей исходной функции потока требуется соглашение о вызовах cdecl, но BeginThread требует соглашения Паскаля), создайте запись, сохраните в ней исходную требуемую функцию потока и вызовите эту функцию в вашей паскалевской функции потока:
690714
691715``` pascal
692716type
693717 TCdeclThreadFunc = function (user_data:Pointer):Pointer;cdecl;
694718
695719 PCdeclThreadFuncData = ^TCdeclThreadFuncData;
696720 TCdeclThreadFuncData = record
697721 Func: TCdeclThreadFunc; //функция cdecl
698722 Data: Pointer; //исходные данные
699723 end;
700724
701725// Паскалевский поток вызывает функцию cdecl
702726function C2P_Translator(FuncData: pointer) : ptrint;
703727var
704728 ThreadData: TCdeclThreadFuncData;
705729begin
706730 ThreadData := PCdeclThreadFuncData(FuncData)^;
707731 Result := ptrint(ThreadData.Func(ThreadData.Data));
708732end;
709733
710734procedure CreatePascalThread;
711735var
712736 ThreadData: PCdeclThreadFuncData;
713737begin
714738 New(ThreadData);
715739 // это желаемая функция потока cdecl
716740 ThreadData^.Func := func;
717741 ThreadData^.Data := user_data;
718742 // это создаст поток Паскаля
719743 BeginThread(@C2P_Translator, ThreadData );
720744end;
721745```
722746
723747- Инициализируйте систему потоков FPC, создав фиктивный поток. Если вы не создадите поток Pascal в своем приложении, система потоков не будет инициализирована (и, следовательно, потоки не будут работать и, следовательно, куча не будет работать правильно).
724748
725749``` pascal
726750type
727751 tc = class(tthread)
728752 procedure execute;override;
729753 end;
730754
731755 procedure tc.execute;
732756 begin
733757 end;
734758
735759{ main program }
736760begin
737761 { инициализация системы потоков}
738762 with tc.create(false) do
739763 begin
740764 waitfor;
741765 free;
742766 end;
743767 { ... ваш последующий код }
744768end.
745769```
746770
747771> **Note:** После инициализации системы потоков среда выполнения может установить для системной переменной IsMultiThread значение true, которое используется подпрограммами FPC для выполнения блокировок здесь и там. Вы не должны устанавливать значение этой переменной вручную.
748772
749773- Если по какой-то причине у вас это не работает, попробуйте этот код в функции внешнего потока:
750774
751775``` pascal
752776function ExternalThread(param: Pointer): LongInt; stdcall;
753777var
754778 tm: TThreadManager;
755779begin
756780 GetThreadManager(tm);
757781 tm.AllocateThreadVars;
758782 InitThread(1000000); // отрегулируйте начальный размер стека здесь
759783
760784 { делаем что-то в потоке здесь ... }
761785
762786 Result:=0;
763787end;
764788```
765789
766790### Идентификация внешних потоков
767791
768792Иногда вы даже не знаете, приходится ли вам иметь дело с внешними потоками (например, если какая-то библиотека Cи выполняет обратный вызов). Этот алгоритм может помочь понять это:
769793
7707941\. Спрашиваем у ОС идентификатор текущего потока при запуске вашего приложения
771795
772796``` pascal
773797GetCurrentThreadID() //windows;
774798GetThreadID() //Darwin/macOS;
775799TThreadID(pthread_self) //Linux;
776800```
777801
7788022\. Спрашиваем еще раз идентификатор текущего потока внутри функции потока и сравниваем его с результатом шага 1.
779803
780804### Откажитесь от использования временных функций
781805
782806``` pascal
783807ThreadSwitch()
784808```
785809
786810> **Note:** Не используйте виндовый трюк со [*Sleep(0)*](https://www.freepascal.org/docs-html/rtl/sysutils/sleep.html), так как это будет работать не на всех платформах.
787811
788812## См. также
789813
790814- [[Streaming components|Компоненты потоковой (stream) передачи данных]]
791815- [[Manager Worker Threads System]]
792816- [[Example of multi-threaded application: array of threads]]
793817- [[Parallel procedures]]
794818- [[Main Loop Hooks]]
795819- [[Asynchronous Calls]]
796820- [Mutlithreading - The Delphi way](http://nickhodges.com/MultiThreadingInDelphi/ToC.html)
797821
798822---
799823*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