Sign in Join
Wiki copytemporary

Multithreaded Application Tutorial/zh CN: what changed

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

OldNewLine
11## 概述
22
33这个页面将试图解释如何在Free Pascal 和 Lazarus上编写和调试一个多线程应用程序。一个多线程应用程序创建两个或多个线程同时执行任务。如果你正需要使用多线程,请阅读"你需要多线程吗?",以确定你是否真的需要;这可以避免些麻烦。
44
55程序中的一个线程被称为主线程,也就是在应用程序启动时由操作系统创建的那个线程。主线程是**唯一**一个可以更新用户界面组件的线程,否则,应用程序可能会挂起。
66
77其主要思想是:应用程序使用第二个线程在后台处其他事情,同时用户可以继续使用主线程工作。
88
99线程的另一个用途是为了更好的响应应用程序。如,你创建了一个应用程序,在应用程序启动时,用户按下按钮启动一个大的任务……处理中……之后,程序停止响应,这让用户觉得应用程序假死了,在用户体验上这很不好。如果大的任务在另一个线程中运行,应用程序保持响应,它就像在闲置,在本例中它是个不错的选择。不过,在启用线程之前,你需要禁用表单按钮,以避用户启动多个线程。
1010
1111还有,服务器应用程序在使用多线程后,它能够同时响应多个客户端的请求。
1212
1313## 你需要多线程吗?
1414
1515如果你刚想使用多线程,而只是想让你的应用程序长时间运行任务而不失去响应,那么,多线程可能超过你的需要。多线程应用程序难以调试并且很复杂,多数情况下你不需要多线程。单线程就足够了。如果你能将耗时的任务分隔成几块,并使用**Application.ProcessMessages**。这种方法让LCL处理所有等待的消息并返回。其核心是,在长时间运行任务时定期调用Application.ProcessMessages以处理用户点击了什么东西,或者重绘进度条等等。
1616
1717例如:读取一个大文件并处理它。 参看 examples/multithreading/singlethreadingexample1.lpi。
1818
1919使用多线程
2020
2121- blocking handles, like network communications(~~屏蔽处理,比如网络通信~~)
2222- 同时使用多个处理器 (SMP)
2323- 算法和库调用,必须通过一个 API 调用,如不能被分解成更小的部分。
2424
2525If you want to use multi-threading to increase speed by using multiple processors simultaneously, check if your current program now uses all 100% resources of 1 core CPU (for example, your program can actively use input-output operations, e.g. writing to file; this takes a lot of time, but doesn't load CPU; in this case your program will not be faster with multiple threads). Also check if optimisation level is set to maximum (3). When switching optimisation level from 1 to 3, a program may become about 5 times faster.
2626
2727~~你想使用多线程使用多个处理器来提高速度,检查当前程序使用1个核心的CPU上的100%资源(如,你的程序可以积极使用输入输出操作,如写入文件等等,这需要大量时间,但CPU没有负载;在这种情况下,你的程序将不会比多种线程快。)检查优化级别设置为最大(3)。切换优化级别1到3,程序运行可能会快5倍。~~
2828
2929## 多线程单元
3030
3131在Windows上你不需要任何特殊单元。
3232
3333然而在Linux、Mac OS X和FreeBSD上,你需要cthreads单元,并且,*必须*项目中第一个引入的单元(项目源文件,通常是 .lpr文件)!
3434
3535所以,你的Lazarus应用程序代码看起来应该是:
3636
3737``` pascal
3838program MyMultiThreadedProgram;
3939{$mode objfpc}{$H+}
4040uses
4141{$ifdef unix}
4242 cthreads,
4343 cmem, // the c memory manager is on some systems much faster for multi-threading(C内存管理器在某些系统中使多线程运行更快)
4444{$endif}
4545 Interfaces, // this includes the LCL widgetset 引入LCL 部件工具箱
4646 Forms
4747 { you can add units here },
4848```
4949
5050如果你忘记了这一点,使用TThread启动应用程序时,你会得到这样的错误:
5151
52-` This binary has no thread support compiled in. (`~~`二进制文件编译时没有线程支持`~~`)`\
53-` Recompile the application with a thread-driver in the program uses clause before other units using thread.(`~~`在引用线程单元前引用了其他单元`~~`)`
52+``` pascal
53+ This binary has no thread support compiled in. (<strike>二进制文件编译时没有线程支持</strike>)
54+ Recompile the application with a thread-driver in the program uses clause before other units using thread.(<strike>在引用线程单元前引用了其他单元</strike>)
55+```
5456
5557> **Note:** 提示"mcount"未找到的错误。在你的单元里包含了多线程代码,你需要添加 cthreads单元或使用智能链接。
5658
5759> **Note:** "Project raised exception class 'RunError(232)'" in procedure SYSTEM_NOTHREADERROR (项目引发的异常类)错误,你的代码里需要线程,你需要添加 cthreads单元。
5860
5961## TThread类
6062
6163下面的例子可以在 examples/multithreading/ 目录中找到。
6264
6365若要创建一个多线程的应用程序,最简单的方法是使用 TThread 类。This class permits the creation of an additional thread (alongside the main thread) in a simple way.(~~这个线程允许在主线程上创建一个附加线程)。~~)通常你只需要重写 2 个方法:创建构造函数(constructor)和执行方法(Execute)。
6466
6567在构造函数中,设置线程运行前初始化需要的变量,在TThread原始构造函数中有一参数Suspended,设置Suspended = True防止在创建后自动启动线程。设置为False,则在在调用Start方法后才执行。
6668
6769> **Note:** 方法摘要在:[自FPC 2.4.4 弃用](http://wiki.freepascal.org/User_Changes_2.4.4#TThread.Suspend_and_TThread.Resume_have_been_deprecated)。它被Start代替。
6870
6971FPC 2.0.1及更高版本,TThread.Create有一个隐含的堆栈大小参数。根据需要,你可以改变默认堆栈的大小。深层递归过程是个很好的例子。如果你不指定堆栈大小,将使用系统默认的大小。
7072
7173重写Execute方法,编写将在线程中执行的代码。
7274
7375TThread 类有一个重要属性:
7476
75-`Terminated : boolean;`
77+```text
78+Terminated : boolean;
79+```
7680
7781If the thread has a loop (and this is typical), the loop should be exited when Terminated is true (it is false by default). Within each pass, the value of Terminated must be checked, and if it is true then the loop should be exited as quickly as is appropriate, after any necessary cleanup. Bear in mind that the Terminate method does not do anything by default: the .Execute method must explicitly implement support for it to quit its job.
7882
7983(~~如果线程中包含循环(这是典型的),当 Terminated 为True时退出循环(默认为 False)。每次循环都将检查 Terminated值,如果为真将退出循环,之后做些必要的清理。牢记 Terminate方法不做任何事情,默认情况下:.Execute 方法实现退出循环后的操作。~~)
8084
8185如上所述,线程不应该与可见组件交互,对可见组件操作应该在主线程的上下文中进行。
8286
8387To do this, a TThread method called Synchronize exists. Synchronize requires a method within the thread (that takes no parameters) as an argument. When you call that method through Synchronize(@MyMethod), the thread execution will be paused, the code of MyMethod will be called from the main thread, and then the thread execution will be resumed.
8488
8589(~~要这样做,可以在TThread 方法调用Synchronize。以同步线程中的方法(不带参数)作为参数。当你调用Synchronize(@MyMethod),线程将暂停执行,并从主线程中调用MyMethod,之后线程恢复。~~)
8690
8791The exact working of Synchronize depends on the platform, but basically it does this: (~~同步的具体工作取决于平台,它基本上是:~~)
8892
8993- 发送消息到主消息队列并进入睡眠
9094- eventually the main thread processes the message and calls MyMethod. This way MyMethod is called without context, that means not during a mouse down event or during paint event, but after.
9195- 主线程执行 MyMethod后,唤醒睡眠的线程和下条消息
9296- 之后,线程继续执行
9397
9498另外,还有一重要属性: TThread: FreeOnTerminate。为True时,线程停止(.Execute 方法)后自动释放。否则,需要手动释放。
9599
96100示例:
97101
98102``` pascal
99103 Type
100104 TMyThread = class(TThread)
101105 private
102106 fStatusText : string;
103107 procedure ShowStatus;
104108 protected
105109 procedure Execute; override;
106110 public
107111 Constructor Create(CreateSuspended : boolean);
108112 end;
109113
110114 constructor TMyThread.Create(CreateSuspended : boolean);
111115 begin
112116 FreeOnTerminate := True;
113117 inherited Create(CreateSuspended);
114118 end;
115119
116120 procedure TMyThread.ShowStatus;
117121 // this method is executed by the mainthread and can therefore access all GUI elements.
118122 begin
119123 Form1.Caption := fStatusText;
120124 end;
121125
122126 procedure TMyThread.Execute;
123127 var
124128 newStatus : string;
125129 begin
126130 fStatusText := 'TMyThread Starting...';
127131 Synchronize(@Showstatus);
128132 fStatusText := 'TMyThread Running...';
129133 while (not Terminated) and ([any condition required]) do
130134 begin
131135 ...
132136 [here goes the code of the main thread loop]
133137 ...
134138 if NewStatus <> fStatusText then
135139 begin
136140 fStatusText := newStatus;
137141 Synchronize(@Showstatus);
138142 end;
139143 end;
140144 end;
141145```
142146
143147在应用程序中,
144148
145149``` pascal
146150 var
147151 MyThread : TMyThread;
148152 begin
149153 MyThread := TMyThread.Create(True); // This way it doesn't start automatically
150154 ...
151155 [Here the code initialises anything required before the threads starts executing]
152156 ...
153157 MyThread.Start;
154158 end;
155159```
156160
157161If you want to make your application more flexible you can create an event for the thread; this way your synchronized method won't be tightly coupled with a specific form or class: you can attach listeners to the thread's event. Here is an example:
158162
159163(~~如果想使你的应用更加灵活,可以创建一个事件的线程;这样一来,synchronized方法将不会被特定窗体或类紧耦合:你可以监听线程的事件。~~)
160164
161165下面是一个例子:
162166
163167``` pascal
164168 Type
165169 TShowStatusEvent = procedure(Status: String) of Object;
166170
167171 TMyThread = class(TThread)
168172 private
169173 fStatusText : string;
170174 FOnShowStatus: TShowStatusEvent;
171175 procedure ShowStatus;
172176 protected
173177 procedure Execute; override;
174178 public
175179 Constructor Create(CreateSuspended : boolean);
176180 property OnShowStatus: TShowStatusEvent read FOnShowStatus write FOnShowStatus;
177181 end;
178182
179183 constructor TMyThread.Create(CreateSuspended : boolean);
180184 begin
181185 FreeOnTerminate := True;
182186 inherited Create(CreateSuspended);
183187 end;
184188
185189 procedure TMyThread.ShowStatus;
186190 // this method is executed by the mainthread and can therefore access all GUI elements.
187191 begin
188192 if Assigned(FOnShowStatus) then
189193 begin
190194 FOnShowStatus(fStatusText);
191195 end;
192196 end;
193197
194198 procedure TMyThread.Execute;
195199 var
196200 newStatus : string;
197201 begin
198202 fStatusText := 'TMyThread Starting...';
199203 Synchronize(@Showstatus);
200204 fStatusText := 'TMyThread Running...';
201205 while (not Terminated) and ([any condition required]) do
202206 begin
203207 ...
204208 [here goes the code of the main thread loop]
205209 ...
206210 if NewStatus <> fStatusText then
207211 begin
208212 fStatusText := newStatus;
209213 Synchronize(@Showstatus);
210214 end;
211215 end;
212216 end;
213217```
214218
215219在应用程序中,
216220
217221``` pascal
218222 Type
219223 TForm1 = class(TForm)
220224 Button1: TButton;
221225 Label1: TLabel;
222226 procedure FormCreate(Sender: TObject);
223227 procedure FormDestroy(Sender: TObject);
224228 private
225229 { private declarations }
226230 MyThread: TMyThread;
227231 procedure ShowStatus(Status: string);
228232 public
229233 { public declarations }
230234 end;
231235
232236 procedure TForm1.FormCreate(Sender: TObject);
233237 begin
234238 inherited;
235239 MyThread := TMyThread.Create(true);
236240 MyThread.OnShowStatus := @ShowStatus;
237241 end;
238242
239243 procedure TForm1.FormDestroy(Sender: TObject);
240244 begin
241245 MyThread.Terminate;
242246
243247 // FreeOnTerminate is true so we should not write:
244248 // MyThread.Free;
245249 inherited;
246250 end;
247251
248252 procedure TForm1.Button1Click(Sender: TObject);
249253 begin
250254 MyThread.Start;
251255 end;
252256
253257 procedure TForm1.ShowStatus(Status: string);
254258 begin
255259 Label1.Caption := Status;
256260 end;
257261```
258262
259263## 特殊处理
260264
261265### Windows堆栈检查
262266
263267There is a potential headache in Windows with Threads if you use the -Ct (stack check) switch. For reasons not so clear the stack check will "trigger" on any TThread.Create if you use the default stack size. The only work-around for the moment is to simply not use -Ct switch. Note that it does NOT cause an exception in the main thread, but in the newly created one. This "looks" like if the thread was never started.
264268
265269A good code to check for this and other exceptions which can occur in thread creation is:
266270
267271如果在Windows使用-CT(堆栈检查)会引发一些问题。 如果你使用默认的堆栈大小在,堆栈检查将在TThread.Create时"触发",原因不是很清楚。 唯一的解决方法是,不使用-CT。请注意,它不会导致主线程的异常,但新创建的却不一定。如果线程从未启动过,它"看起来"会。
268272
269273下面的代码来检查它,用于在线程创建时抛出异常:
270274
271275``` pascal
272276MyThread := TThread.Create(False);
273277if Assigned(MyThread.FatalException) then
274278 raise MyThread.FatalException;
275279```
276280
277281This code will assure that any exception which occurred during thread creation will be raised in your main thread.
278282
279283~~上面的代码,将在某个线程创建时抛出任何异常在主线程中。~~
280284
281285### 多线程包
282286
283287Packages which uses multi-threading should add the **-dUseCThreads** flag to the custom usage options. Open the package editor of the package, then Options \> Usage \> Custom and add *-dUseCThreads*. This will define this flag to all projects and packages using this package, including the IDE. The IDE and all new applications created by the IDE have already the following code in their .lpr file:
284288
285289``` pascal
286290uses
287291 {$IFDEF UNIX}{$IFDEF UseCThreads}
288292 cthreads,
289293 cmem, // the c memory manager is on some systems much faster for multi-threading
290294 {$ENDIF}{$ENDIF}
291295```
292296
293297### Heaptrc
294298
295299You can not use the -gh switch with the *cmem* unit. The -gh switch uses the heaptrc unit, which extends the heap manager. Therefore the **heaptrc** unit must be used **after** the **cmem** unit.
296300
297301~~你不能使用-gh选项与 *cmem* 单元。-gh使用了heaptrc单元,它扩展了堆管理器。因此**heaptrc** 必须在**cmem**单元后引用。~~
298302
299303``` pascal
300304uses
301305 {$IFDEF UNIX}{$IFDEF UseCThreads}
302306 cthreads,
303307 cmem, // the c memory manager is on some systems much faster for multi-threading
304308 {$ENDIF}{$ENDIF}
305309 heaptrc,
306310```
307311
308312## SMP支持
309313
310314好消息是,如果你的应用程序在多线程上工作正常,看来SMP已经启用。
311315
312316## Lazarus调试多线程应用程序
313317
314318Lzarus上使用[GDB](http://zh.wikipedia.org/zh-cn/GDB)进行调试,它功能齐全、稳定。不过,在一些Linux发行版上存在一些问题。
315319
316320### 调试输出
317321
318322In a single threaded application, you can simply write to console/terminal/whatever and the order of the lines is the same as they were written. In multi-threaded application things are more complicated. If two threads are writing, say a line is written by thread A before a line by thread B, then the lines are not necessarily written in that order. It can even happen, that a thread writes its output, while the other thread is writing a line.While under linux (maybe) you'll get proper DebugLn() output, under win32 you can get exceptions (probably DiskFull) because of DebugLn() usage outside of main thread.So, to avoid headaches use DebugLnThreadLog() mentioned below.
319323
320324(在一个单线程的应用程序中,你可以很容易的写入(输出信息)到控制台/终端/其他,因为写入顺序是一样的。
321325
322326在多线程应用程序中这样做会变得很复杂。如果两个线程在写,线程A在线程B之前输出一行,似乎输出结果并不会按写入顺序显示。甚至会发生,一个线程写入一个线程输出,其他的线程在写。在Linux下使用DebugLn()你会得到正确的输出结果。在Win32下可能得到异常(或许是DiskFull),它是因为,在主线程外使用DebugLn()。
323327
324328所以,为了避免这类问题,使用DebugLnThreadLog(),下面将会提到:)
325329
326330LCLProc 单元包含几个过程,让每个线程写入它自己的日志文件:
327331
328332``` pascal
329333 procedure DbgOutThreadLog(const Msg: string); overload;
330334 procedure DebuglnThreadLog(const Msg: string); overload;
331335 procedure DebuglnThreadLog(Args: array of const); overload;
332336 procedure DebuglnThreadLog; overload;
333337```
334338
335339示例: 不是*writeln('Some text ',123);* 而是,使用
336340
337-` DebuglnThreadLog(['Some text ',123]);`
341+```text
342+ DebuglnThreadLog(['Some text ',123]);
343+```
338344
339345这将在**Log\<PID\>.txt**文件中添加一行'Some text 123'。\<PID\>是当前线程的进程ID。
340346
341347在每次运行前删除日志文件是个不错的选择:
342348
343-` rm -f Log* && ./project1`
349+```text
350+ rm -f Log* && ./project1
351+```
344352
345353### Linux
346354
347355如果你尝试在Linux调试多线程应用程序,你将会遇到一个很大的问题:
348356
349357X服务器上的桌面管理器可能会**挂掉**。如应用程序已捕获鼠标/键盘gdb暂停,X服务器等待应用程序响应。
350358
351359发生这种情况时,你可以使用另一个用户登录计算机杀掉gdb或按Ctrl+Alt+F3退出会话并杀掉gdb。
352360
353361或者,你重新启动窗口管理器:输入 /etc/init.d/gdm restart。
354362
355363这将重新启动桌面管理器,让你回到桌面。
356364
357365Since it depends where gdb stops your program in some cases some tricks may help: 为Ubuntu x64设置项目选项用于调试所需的额外信息文件...
358366
359-` 工程菜单 -> 工程选项 -> 编译选项中的调试项,勾选`**`使用外部gdb调试语法文件(-Xg)`**`。`
367+```text
368+ 工程菜单 -> 工程选项 -> 编译选项中的调试项,勾选使用外部gdb调试语法文件(-Xg)。
369+```
360370
361371另一种选择是,打开另一个X桌面,在里面运行 IDE/GDB和其他应用程序,因此,只会在测试桌面中冻结。创建一个新的X实例:
362372
363-` X :1 &`
373+```text
374+ X :1 &
375+```
364376
365377It will open, and when you switch to another desktop (the one you are working with pressing CTRL+ALT+F7), you will be able to go back to the new graphical desktop with CTRL+ALT+F8 (if this combination does not work, try with CTRL+ALT+F2... this one worked on [Slackware](http://www.slackware.com)).
366378
367379(~~这将打开,按Ctrl+Alt+F7切换到另一个桌面,Ctrl+Alt+F8切换到原桌面(桌面快捷键没效果,尝试按Ctrl+Alt+F2,它工作在[Slackware](http://www.slackware.com))。~~)
368380
369381Then you could, if you want, create a desktop session on the X started with:
370382
371383(~~之后,如果你愿意,你可以在X开始时创建会话:~~)
372384
373-` gnome-session --display=:1 &`
385+```text
386+ gnome-session --display=:1 &
387+```
374388
375389Then, in Lazarus, on the run parameters dialog for the project, check "Use display" and enter :1.
376390
377391(~~之后在Lazarus运行参数对话框中检查Use display并设置为:1。~~)
378392
379393现在应用程序运行在第二个X服务器上,你可以在第一个X服务器上调试它。。
380394
381395在Windows及Linux上的Free Pascal 2.0 和Lazarus 0.9.10 测试。
382396
383397------------------------------------------------------------------------
384398
385399Instead of creating a new X session, one can use [Xnest](http://en.wikipedia.org/wiki/Xnest). Xnest is a X session on a window. Using it X server didn't lock while debugging threads, and it's much easier to debug without keeping changing terminals.
386400
387401(~~而不是创建一个新的X会话,可以使用[Xnest](http://en.wikipedia.org/wiki/Xnest)。Xnest是X会话窗口,它使用X服务器不锁定调试线程,及更容易调试并改变终端。~~)
388402
389403运行 Xnest 命令
390404
391-` Xnest :1 -ac`
405+```text
406+ Xnest :1 -ac
407+```
392408
393409创建一个X会话:1,并禁用访问控制。
394410
395411## Lazarus 部件工具箱接口
396412
397413The win32, the gtk and the carbon interfaces support multi-threading. This means, TThread, critical sections and Synchronize work. But they are not thread safe. This means only one thread at a time can access the LCL. And since the main thread should never wait for another thread, it means only the main thread is allowed to access the LCL, which means anything that has to do with TControl, Application and LCL widget handles. There are some thread safe functions in the LCL. For example most of the functions in the FileUtil unit are thread safe.
398414
399415(~~在Win32 中,gtk 和carbon接口支持多线程。这意味着,TThread、关键区域和同步工作,但它们不是线程安全的。也就是说只有一个线程在同一时间可以访问LCL。由于主线程不应该等待另一个线程,这使得只有主线程访问LCL,这意味着任何TControl,应用程序和LCL控件句柄。LCL中有一些线程安全函数。例如大部分的 FileUtil 单元中的函数是[线程安全](http://zh.wikipedia.org/wiki/%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8)的。~~)
400416
401417### 使用SendMessage/PostMessage进行线程间通信
402418
403419在应用程序中只有一个线程可以调用LCL API,通常是主线程。其他线程可以通过一些间接的方法调用LCL,使用SendMessage、PostMessage是不错的选择。LCLIntf.SendMessage 和 LCLIntf.PostMessage将发送一条消息到应用程序的消息队列中。
404420
405421参看这些例程文档
406422
407423- [[doc:lcl/lclintf/sendmessage.html|SendMessage]]
408424- [[doc:lcl/lclintf/postmessage.html|PostMessage]]
409425
410426The difference between SendMessage and PostMessage is the way that they return control to the calling thread. With SendMessage control is not returned until the window that the message was sent to has completed processing the sent message, however with PostMessage control is returned immediately.
411427
412428(~~SendMessage 和 PostMessage区别在于处理调用线程的方式。SendMessage 在接收到返回值后才继续执行,而PostMessage则不会等待。~~)
413429
414430Here is an example of how a secondary thread could send text to be displayed in an LCL control to the main thread:
415431
416432``` pascal
417433const
418434 WM_GOT_ERROR = LM_USER + 2004;
419435 WM_VERBOSE = LM_USER + 2005;
420436
421437procedure VerboseLog(Msg: string);
422438var
423439 PError: PChar;
424440begin
425441 if MessageHandler = 0 then Exit;
426442 PError := StrAlloc(Length(Msg)+1);
427443 StrCopy(PError, PChar(Msg));
428444 PostMessage(formConsole.Handle, WM_VERBOSE, Integer(PError), 0);
429445end;
430446```
431447
432448及如何从窗口中处理这个消息:
433449
434450``` pascal
435451const
436452 WM_GOT_ERROR = LM_USER + 2004;
437453 WM_VERBOSE = LM_USER + 2005;
438454
439455type
440456 { TformConsole }
441457
442458 TformConsole = class(TForm)
443459 DebugList: TListView;
444460 // ...
445461 private
446462 procedure HandleDebug(var Msg: TLMessage); message WM_VERBOSE;
447463 end;
448464
449465var
450466 formConsole: TformConsole;
451467
452468implementation
453469
454470....
455471
456472{ TformConsole }
457473
458474procedure TformConsole.HandleDebug(var Msg: TLMessage);
459475var
460476 Item: TListItem;
461477 MsgStr: PChar;
462478 MsgPasStr: string;
463479begin
464480 MsgStr := PChar(Msg.wparam);
465481 MsgPasStr := StrPas(MsgStr);
466482 Item := DebugList.Items.Add;
467483 Item.Caption := TimeToStr(SysUtils.Now);
468484 Item.SubItems.Add(MsgPasStr);
469485 Item.MakeVisible(False);
470486 //f/TrayControl.SetError(MsgPasStr);
471487end;
472488
473489end.
474490```
475491
476492## 临界区
477493
478494A *critical section* is an object used to make sure, that some part of the code is executed only by one thread at a time. A critical section needs to be created/initialized before it can be used and be freed when it is not needed anymore.
479495
480496(~~*[临界区](http://zh.wikipedia.org/wiki/%E4%B8%B4%E7%95%8C%E5%8C%BA)*用来确保代码的某些部分同一时间只能由一个线程执行。临界区需要被创建/初始化后才能使用,不需要时释放它。~~)
481497
482498临界区通常使用这种方式:
483499
484500声明 (全局范围内所有线程可以访问):
485501
486-` MyCriticalSection: TRTLCriticalSection;`
502+```text
503+ MyCriticalSection: TRTLCriticalSection;
504+```
487505
488506初始化临界区:
489507
490-` InitializeCriticalSection(MyCriticalSection);`
508+```text
509+ InitializeCriticalSection(MyCriticalSection);
510+```
491511
492512运行一些线程,只做
493513
494514``` pascal
495515EnterCriticalSection(MyCriticalSection);
496516try
497517 // 访问变量、写入文件、发送网络数据包等等
498518finally
499519 LeaveCriticalSection(MyCriticalSection);
500520end;
501521```
502522
503523所有线程结束后,释放它:
504524
505-` DeleteCriticalSection(MyCriticalSection);`
525+```text
526+ DeleteCriticalSection(MyCriticalSection);
527+```
506528
507529As an alternative, you can use a TCriticalSection object. The creation does the initialization, the Enter method does the EnterCriticalSection, the Leave method does the LeaveCriticalSection and the destruction of the object does the deletion.
508530
509531(~~作为代替者,你可以使用TCriticalSection对象。创建即初始化临界区,进入临界区使用 EnterCriticalSection,离开时使用 LeaveCriticalSection,对象销毁时删除临界区。~~)
510532
511533例如:5个线程递增计数器。 查看 lazarus/examples/multithreading/criticalsectionexample1.lpi
512534
513535**注意**: There are two sets of the above 4 functions. The RTL and the LCL ones. The LCL ones are defined in the unit LCLIntf and LCLType. Both work pretty much the same. You can use both at the same time in your application, but you should not use a RTL function with an LCL Critical Section and vice versus.
514536
515537### 共享变量
516538
517539如果一些线程共享变量,它是只读的,那么就没有什么好担心的,直接读取就可以了。
518540
519541但是如果一个或多个线程想改变变量,那么你必须确保,同一时间只有一个线程访问该变量。
520542
521543示例:5线程递增计数器。 查看 lazarus/examples/multithreading/criticalsectionexample1.lpi
522544
523545## 等待另一个线程
524546
525547如果一个线程A需要线程B的结果,那么它必须等待,直到B完成。
526548
527549**重要:**主线程不应该等待另一个线程,不是使用Synchronize(见上文)。
528550
529551查看示例: lazarus/examples/multithreading/waitforexample1.lpi
530552
531553``` pascal
532554{ TThreadA }
533555
534556procedure TThreadA.Execute;
535557begin
536558 Form1.ThreadB:=TThreadB.Create(false);
537559 // create event
538560 WaitForB:=RTLEventCreate;
539561 while not Application.Terminated do begin
540562 // wait infinitely (until B wakes A)
541563 RtlEventWaitFor(WaitForB);
542564 writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter));
543565 end;
544566end;
545567
546568{ TThreadB }
547569
548570procedure TThreadB.Execute;
549571var
550572 i: Integer;
551573begin
552574 Counter:=0;
553575 while not Application.Terminated do begin
554576 // B: Working ...
555577 Sleep(1500);
556578 inc(Counter);
557579 // wake A
558580 RtlEventSetEvent(Form1.ThreadA.WaitForB);
559581 end;
560582end;
561583```
562584
563585> **Note:** RtlEventSetEvent can be called before RtlEventWaitFor. Then RtlEventWaitFor will return immediately. Use RTLeventResetEvent to clear a flag.
564586
565587> **Note:** ~~RtlEventSetEvent可以在RtlEventWaitFor之前被调用。之后RtlEventWaitFor将立即返回。使用RTLeventResetEvent清除标记。~~
566588
567589## Fork
568590
569591When forking in a multi-threaded application, be aware that any threads created and running BEFORE the fork (or fpFork) call, will NOT be running in the child process. As stated on the fork() man page, any threads that were running before the fork call, their state will be undefined.
570592
571593So be aware of any threads initializing before the call (including on the initialization section). They will NOT work.
572594
573595## 并行程序/循环
574596
575597多线程的一个特殊情况是并行运行一个程序。参见[[Parallel procedures|并行程序]]。
576598
577599## 分布式计算
578600
579601The next higher steps after multi threading is running the threads on multiple machines.
580602
581603- 可以使用TCP组件像synapse、lnet 或 indy通信。这给你最大的灵活性和主要用于松散连接的客户端/服务器应用程序。
582604- 你可以使用消息传递库,像[[MPICH]],用于HPC(高性能计算)集群上。
583605
584606## 外部线程
585607
586608To make Free Pascal's threading system work properly, each newly created FPC thread needs to be initialized (more exactly, the exception, I/O system and threadvar system per thread needs to be initialized so threadvars and heap are working). That is fully automatically done for you if you use BeginThread (or indirectly by using the TThread class). However, if you use threads that were created without BeginThread (i.e. external threads), additional work (currently) might be required. External threads also include those that were created in external C libraries (.DLL/.so).
587609
588610Things to consider when using external threads (might not be needed in all or future compiler versions):
589611
590612- Do not use external threads at all - use FPC threads. If can you can get control over how the thread is created, create the thread by yourself by using BeginThread.
591613
592614If the calling convention doesn't fit (e.g. if your original thread function needs cdecl calling convention but BeginThread needs pascal convention, create a record, store the original required thread function in it, and call that function in your pascal thread function:
593615
594616``` pascal
595617type
596618 TCdeclThreadFunc = function (user_data:Pointer):Pointer;cdecl;
597619
598620 PCdeclThreadFuncData = ^TCdeclThreadFuncData;
599621 TCdeclThreadFuncData = record
600622 Func: TCdeclThreadFunc; //cdecl function
601623 Data: Pointer; //original data
602624 end;
603625
604626// The Pascal thread calls the cdecl function
605627function C2P_Translator(FuncData: pointer) : ptrint;
606628var
607629 ThreadData: TCdeclThreadFuncData;
608630begin
609631 ThreadData := PCdeclThreadFuncData(FuncData)^;
610632 Result := ptrint(ThreadData.Func(ThreadData.Data));
611633end;
612634
613635procedure CreatePascalThread;
614636var
615637 ThreadData: PCdeclThreadFuncData;
616638begin
617639 New(ThreadData);
618640 // this is the desired cdecl thread function
619641 ThreadData^.Func := func;
620642 ThreadData^.Data := user_data;
621643 // this creates the Pascal thread
622644 BeginThread(@C2P_Translator, ThreadData );
623645end;
624646```
625647
626648- Initialize the FPC's threading system by creating a dummy thread. If you don't create any Pascal thread in your app, the thread system won't be initialized (and thus threadvars won't work and thus heap will not work correctly).
627649
628650``` pascal
629651type
630652 tc = class(tthread)
631653 procedure execute;override;
632654 end;
633655
634656 procedure tc.execute;
635657 begin
636658 end;
637659
638660{ main program }
639661begin
640662 { initialise threading system }
641663 with tc.create(false) do
642664 begin
643665 waitfor;
644666 free;
645667 end;
646668 { ... your code follows }
647669end.
648670```
649671
650672(After the threading system is initialized, the runtime may set the system variable "IsMultiThread" to true which is used by FPC routines to perform locks here and there. You should not set this variable manually.)
651673
652674- If for some reason this doesn't work for you, try this code in your external thread function:
653675
654676``` pascal
655677function ExternalThread(param: Pointer): LongInt; stdcall;
656678var
657679 tm: TThreadManager;
658680begin
659681 GetThreadManager(tm);
660682 tm.AllocateThreadVars;
661683 InitThread(1000000); // adjust inital stack size here
662684
663685 { do something threaded here ... }
664686
665687 Result:=0;
666688end;
667689```
668690
669691### 识别外部线程
670692
671693有时你甚至不知道是否要处理外部线程 (如对C 库进行回调)。这个可以帮助你来分析:
672694
6736951\. 询问操作系统当前应用程序线程的ID
674696
675697``` pascal
676698GetCurrentThreadID() // Windows;
677699GetThreadID() // Darwin/OSX;
678700TThreadID(pthread_self) // Linux;
679701```
680702
6817032\. 再次询问线程内部的当前线程的ID和步骤1结果对比
682704
683705### 放弃时间片
684706
685707ThreadSwitch()
686708
687709> **Note:** 不要使用Windows的 *Sleep(0)*,因为它不会在所有平台上工作。
688710
689711## 参见
690712
691713- [[Streaming components|流媒体组件]]
692714- [[Manager Worker Threads System|管理工作线程系统]]
693715- [[Example of multi-threaded application: array of threads|多线程应用示例:数组线程]]
694716- [[Parallel procedures|并行程序]]
695717- [[Main Loop Hooks|主循环钩子]]
696718- [[Asynchronous Calls|异步调用]]
697719
698720---
699721*From the Free Pascal wiki, page "Multithreaded Application Tutorial/zh CN", as it stood on 2020-02-20 after a change by Trev, 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