Multithreaded Application Tutorial/zh CN: what changed
Revision 3 to 4. Removed lines have a minus sign; added lines have a plus sign.
| Old | New | Line | |
|---|---|---|---|
| 1 | 1 | ## 概述 | |
| 2 | 2 | ||
| 3 | - | ||
| 4 | - | ||
| 5 | - | ||
| 6 | - | ||
| 7 | - | ||
| 3 | + | 本文力图说清楚如何用 Free Pascal 和 Lazarus 编写和调试多线程应用。多线程应用会创建两个以上同时运行的线程。如果对多线程还不熟悉,请阅读“是否需要多线程”一节,以确定是否真的需要;这可能会避免很多麻烦。 | |
| 8 | 4 | ||
| 9 | - | ||
| 5 | + | 多线程应用只有一个主线程,也即应用启动时由操作系统创建的那个线程。更新用户界面组件的**只能** 是主线程,否则应用可能会被挂起。 | |
| 10 | 6 | ||
| 11 | - | ||
| 7 | + | 多线程的主要思路是:应用可在第二个线程中后台处理一些任务,而用户可以继续使用主线程工作。 | |
| 12 | 8 | ||
| 13 | - | ||
| 9 | + | 线程的另一个用途是让应用具备更好的响应能力。假设用户按下某个按钮后,应用开始处理一项大型任务……在处理过程中屏幕就会停止响应,用户会以为应用停滞不动了,用户体验就不大好,或引起误导。如果大型任务运行于另一个线程中,则应用几乎就能像空闲时一样保持响应。这时在启动大型任务的工作线程之前,最好是禁用窗体上的按钮,以免用户为同一任务启动多个线程。 | |
| 14 | 10 | ||
| 15 | - | ||
| 11 | + | 多线程的另一个用途,可能就是服务器应用,以便能同时响应多个客户端的请求。 | |
| 16 | 12 | ||
| 17 | - | ||
| 13 | + | ## 是否需要多线程? | |
| 18 | 14 | ||
| 19 | - | ||
| 15 | + | 如果对多线程编程不太熟悉,并且只是希望应用在执行稍长任务时能提升响应速度,那么可能并不一定需要用到多线程。多线程应用肯定更难调试,且往往更加复杂;很多情况下并不需要用到多线程。单线程就足以应付了。如果能将耗时任务拆分为多个部分,则应使用 Application.ProcessMessages 方法。此方法允许 LCL 处理所有等待的消息并返回。主要的思路是在执行长时间任务的过程中定期调用 Application.ProcessMessages,检测用户的点击,或者重绘进度条等。 | |
| 20 | 16 | ||
| 21 | - | ||
| 22 | - | ||
| 23 | - | ||
| 17 | + | 例如:读取大文件并进行处理。参见示例文件 examples/multithreading/singlethreadingexample1.lpi。 | |
| 24 | 18 | ||
| 25 | - | ||
| 19 | + | 多线程仅适用于: | |
| 26 | 20 | ||
| 27 | - | ||
| 21 | + | * 阻塞句柄,比如网络通信 | |
| 22 | + | * 同时使用多个处理器 (SMP) | |
| 23 | + | * 必须通过一次 API 调用完成的算法和库调用,无法分解成更小的部分 | |
| 28 | 24 | ||
| 29 | - | ||
| 25 | + | 若要通过多线程同时使用多个处理器以提高速度,请先检查程序目前是否已经跑满了单核 CPU 的资源(例如,程序可能是在做大量输入输出操作,比如写入文件,这很耗时但并不占用大量 CPU 资源;这种情况下,即便用了多线程也不会更快)。另外,请检查优化级别是否已设为最高(3级)。将优化级别从1级升至3级,程序运行速度可能会提升5倍左右。 | |
| 30 | 26 | ||
| 31 | - | ||
| 27 | + | ## 多线程应用所需单元 | |
| 32 | 28 | ||
| 33 | - | ||
| 29 | + | Windows 系统下实现多线程应用,不需要用到什么特别的单元。但是在 Linux、macOS 和 FreeBSD 系统中,需要用到 cthreads 单元,还必须是项目第一个引用的单元(程序源代码通常是 .lpr 文件)!有时 cthreads、cmem 和 cwstrings 等多个单元都要放在前面时,由于各单元之间的依赖关系,合理的顺序是cmem、cthreads、cwstrings。 | |
| 34 | 30 | ||
| 35 | - | ||
| 31 | + | ||
| 36 | 32 | ||
| 37 | - | ||
| 33 | + | 所以 Lazarus 应用的源代码应如下所示: | |
| 34 | + | ``` | |
| 38 | 35 | program MyMultiThreadedProgram; | |
| 39 | - | ||
| 40 | - | ||
| 41 | - | ||
| 42 | - | ||
| 43 | - | ||
| 44 | - | ||
| 45 | - | ||
| 46 | - | ||
| 47 | - | ||
| 36 | + | {$mode objfpc}{$H+} | |
| 37 | + | uses | |
| 38 | + | {$ifdef unix} | |
| 39 | + | cthreads, | |
| 40 | + | cmem, // 在某些系统中,多线程时 c 语言的内存管理速度会快很多 | |
| 41 | + | {$endif} | |
| 42 | + | Interfaces, // 引入 LCL 可视化组件 | |
| 43 | + | Forms | |
| 44 | + | { 在此加入其它单元 }, | |
| 48 | 45 | ``` | |
| 49 | 46 | ||
| 50 | - | ||
| 51 | - | ||
| 52 | - | ||
| 53 | - | ||
| 54 | - | ||
| 47 | + | 如果忘记了 TThread 引用顺序,在应用启动时会显示如下错误: | |
| 55 | 48 | ``` | |
| 56 | - | ||
| 57 | - | ||
| 58 | - | ||
| 59 | - | ||
| 60 | - | ||
| 61 | - | ||
| 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 | + | ``` | |
| 62 | 52 | ||
| 63 | - | ||
| 53 | + |  **Note:** 如果链接时报错找不到“mcount”,可能是因为引用了包含多线程代码的某个单元,此时需添加 cthreads 单元或启用[[smart linking|智能链接]]功能。 | |
| 64 | 54 | ||
| 65 | - | ||
| 55 | + |  **Note:** 如果报错“Project raised exception class 'RunError(232)' in procedure SYSTEM_NOTHREADERROR”,说明代码用到了线程,需添加 cthreads 单元的引用。 | |
| 66 | 56 | ||
| 67 | - | ||
| 57 | + | ||
| 68 | 58 | ||
| 69 | - | ||
| 59 | + | ## 纯用 FPC 实现的代码示例 | |
| 70 | 60 | ||
| 71 | - | ||
| 61 | + | 以下是非常简单的示例。在 Win7 及 FPC 3.0.4 中调试通过。 | |
| 62 | + | ``` | |
| 63 | + | Program ThreadTest; | |
| 64 | + | {test multi threading capability } | |
| 65 | + | { | |
| 66 | + | OUTPUT | |
| 67 | + | thread 1 started | |
| 68 | + | thread 1 thri 0 Len(S)= 1 | |
| 69 | + | thread 1 thri 1 Len(S)= 2 | |
| 70 | + | thread 1 thri 2 Len(S)= 3 | |
| 71 | + | thread 1 thri 3 Len(S)= 4 | |
| 72 | + | thread 1 thri 4 Len(S)= 5 | |
| 73 | + | thread 1 thri 5 Len(S)= 6 | |
| 74 | + | thread 1 thri 6 Len(S)= 7 | |
| 75 | + | thread 1 thri 7 Len(S)= 8 | |
| 76 | + | thread 1 thri 8 Len(S)= 9 | |
| 77 | + | thread 1 thri 9 Len(S)= 10 | |
| 78 | + | thread 1 thri 10 Len(S)= 11 | |
| 79 | + | thread 1 thri 11 Len(S)= 12 | |
| 80 | + | thread 1 thri 12 Len(S)= 13 | |
| 81 | + | thread 1 thri 13 Len(S)= 14 | |
| 82 | + | thread 1 thri 14 Len(S)= 15 | |
| 83 | + | thread 2 started | |
| 84 | + | thread 3 started | |
| 85 | + | thread 1 thri 15 Len(S)= 16 | |
| 86 | + | thread 2 thri 0 Len(S)= 1 | |
| 87 | + | thread 3 thri 0 Len(S)= 1 | |
| 88 | + | thread 1 thri 16 Len(S)= 17 | |
| 89 | + | ... | |
| 90 | + | ... | |
| 91 | + | thread 5 thri 997 Len(S)= 998 | |
| 92 | + | thread 5 thri 998 Len(S)= 999 | |
| 93 | + | thread 5 thri 999 Len(S)= 1000 | |
| 94 | + | thread 5 finished | |
| 95 | + | thread 10 thri 828 Len(S)= 829 | |
| 96 | + | thread 9 thri 675 Len(S)= 676 | |
| 97 | + | thread 4 thri 656 Len(S)= 657 | |
| 98 | + | thread 10 thri 829 Len(S)= 830 | |
| 99 | + | thread 9 thri 676 Len(S)= 677 | |
| 100 | + | thread 9 thri 677 Len(S)= 678 | |
| 101 | + | thread 10 thri 830 Len(S)= 831 | |
| 102 | + | thread 10 thri 831 Len(S)= 832 | |
| 103 | + | thread 10 thri 832 Len(S)= 833 | |
| 104 | + | thread 10 thri 833 Len(S)= 834 | |
| 105 | + | thread 10 thri 834 Len(S)= 835 | |
| 106 | + | thread 10 thri 835 Len(S)= 836 | |
| 107 | + | thread 10 thri 836 Len(S)= 837 | |
| 108 | + | thread 10 thri 837 Len(S)= 838 | |
| 109 | + | thread 10 thri 838 Len(S)= 839 | |
| 110 | + | thread 10 thri 839 Len(S)= 840 | |
| 111 | + | thread 9 thri 678 Len(S)= 679 | |
| 112 | + | ... | |
| 113 | + | ... | |
| 114 | + | thread 4 thri 994 Len(S)= 995 | |
| 115 | + | thread 4 thri 995 Len(S)= 996 | |
| 116 | + | thread 4 thri 996 Len(S)= 997 | |
| 117 | + | thread 4 thri 997 Len(S)= 998 | |
| 118 | + | thread 4 thri 998 Len(S)= 999 | |
| 119 | + | thread 4 thri 999 Len(S)= 1000 | |
| 120 | + | thread 4 finished | |
| 121 | + | 10 | |
| 122 | + | ||
| 123 | + | } | |
| 124 | + | ||
| 125 | + | uses | |
| 126 | + | {$ifdef unix}cthreads, {$endif} sysutils; | |
| 127 | + | ||
| 128 | + | const | |
| 129 | + | threadcount = 10; | |
| 130 | + | stringlen = 1000; | |
| 131 | + | ||
| 132 | + | var | |
| 133 | + | finished : longint; | |
| 134 | + | ||
| 135 | + | threadvar | |
| 136 | + | thri : ptrint; | |
| 137 | + | ||
| 138 | + | function f(p : pointer) : ptrint; | |
| 139 | + | var | |
| 140 | + | s : ansistring; | |
| 141 | + | begin | |
| 142 | + | Writeln('thread ',longint(p),' started'); | |
| 143 | + | thri:=0; | |
| 144 | + | while (thri<stringlen) do begin | |
| 145 | + | s:=s+'1'; { create a delay } | |
| 146 | + | writeln('thread ',longint(p),' thri ',thri,' Len(S)= ',length(s)); | |
| 147 | + | inc(thri); | |
| 148 | + | end; | |
| 149 | + | Writeln('thread ',longint(p),' finished'); | |
| 150 | + | InterLockedIncrement(finished); | |
| 151 | + | f:=0; | |
| 152 | + | end; | |
| 153 | + | ||
| 154 | + | ||
| 155 | + | var | |
| 156 | + | i : longint; | |
| 157 | + | ||
| 158 | + | Begin | |
| 159 | + | finished:=0; | |
| 160 | + | for i:=1 to threadcount do | |
| 161 | + | BeginThread(@f,pointer(i)); | |
| 162 | + | while finished<threadcount do ; | |
| 163 | + | Writeln(finished); | |
| 164 | + | End. | |
| 165 | + | ``` | |
| 72 | 166 | ||
| 73 | - | ||
| 167 | + | ## TThread 类 | |
| 74 | 168 | ||
| 75 | - | ||
| 169 | + | 以下例子位于 examples/multithreading/ 目录中。 | |
| 76 | 170 | ||
| 77 | - | ||
| 78 | - | ||
| 79 | - | ||
| 171 | + | 创建多线程应用最简单的方法就是用 TThread 类,能以简单的方式创建一个额外的线程(与主线程并行)。通常仅需重写两个方法:构造函数 Create 和 Execute 方法。 | |
| 80 | 172 | ||
| 81 | - | ||
| 173 | + | 构造函数中需要进行线程运行的准备工作,设置变量或属性的初始值。TThread 的原始构造函数需一个 Suspended 参数。正如其名, Suspended 设为 True 会阻止线程创建后自动启动。如果 Suspended 设为 False,线程在创建后将立即开始运行。如果线程是以挂起状态创建的,则只会在调用 Start 方法后运行。 | |
| 82 | 174 | ||
| 83 | - | ||
| 175 | + |  **Note:** Resume 方法自[[User Changes 2.4.4|FPC 2.4.4 后已弃用]],已由 Start 替代。 | |
| 84 | 176 | ||
| 85 | - | ||
| 177 | + | 自 FPC 2.0.1 以后,TThread.Create 还有一个隐式的栈大小参数。可以按需修改所创建线程的默认栈大小。线程中的函数深度递归调用,就是需要扩大栈的很好例子。如未指定栈大小参数,则会采用操作系统的默认大小。 | |
| 86 | 178 | ||
| 87 | - | ||
| 179 | + | 要在线程中执行的代码,通过重写 Execute 方法来完成。 | |
| 88 | 180 | ||
| 89 | - | ||
| 181 | + | TThread 类有一个重要属性:Terminated : boolean; | |
| 90 | 182 | ||
| 91 | - | ||
| 183 | + | 如果线程中包含循环(很常见),那么当 Terminated 为 true 时(默认为 false)应退出循环。每次迭代均须检查 Terminated 值,为 true 则应在完成必要的清理工作后尽快退出循环。~~请记住,Terminate 方法默认不执行任何操作:必须在 Execute 方法中明确实现线程退出工作的机制。~~ Terminate 方法的工作只是将 Terminated 属性设为 True。 | |
| 92 | 184 | ||
| 93 | - | ||
| 94 | - | ||
| 95 | - | ||
| 96 | - | ||
| 185 | + | 如前所述,线程不应与可视化组件发生交互。对可视化组件的更新必须在主线程中进行。 | |
| 97 | 186 | ||
| 98 | - | ||
| 187 | + | 为实现与主线程的协作,TThread 类提供了 Synchronize 方法。Synchronize 需要一个不带参数的线程内部方法作为参数。以 Synchronize(@MyMethod) 的方式调用 MyMethod 时,线程将暂停运行,MyMethod 的代码将在主线程中调用,然后线程继续运行。 | |
| 99 | 188 | ||
| 100 | - | ||
| 189 | + | Synchronize 的准确实现方式依不同平台而定,但基本任务是: | |
| 101 | 190 | ||
| 102 | - | ||
| 103 | - | ||
| 104 | - | ||
| 105 | - | ||
| 106 | - | ||
| 107 | - | ||
| 108 | - | ||
| 109 | - | ||
| 110 | - | ||
| 111 | - | ||
| 112 | - | ||
| 191 | + | * 发送一条消息到主消息队列,然后进入休眠状态; | |
| 192 | + | * 主线程最终会处理这条消息,并调用 MyMethod。MyMethod 以没有上下文的方式调用,也就是不会在鼠标按下或界面绘制事件中,而是在这些事件之后调用; | |
| 193 | + | * 主线程执行完 MyMethod 后,会唤醒休眠线程,并处理下一条消息; | |
| 194 | + | * 线程继续运行。 | |
| 113 | 195 | ||
| 114 | - | ||
| 115 | - | ||
| 116 | - | ||
| 117 | - | ||
| 118 | - | ||
| 196 | + | TThread 还有一个重要的属性:FreeOnTerminate。如果 FreeOnTerminate 为 true,那么线程停止执行(Execute 方法)后,线程对象会自动释放。否则,应用需手动释放线程。 | |
| 119 | 197 | ||
| 120 | - | ||
| 121 | - | ||
| 122 | - | ||
| 123 | - | ||
| 124 | - | ||
| 125 | - | ||
| 126 | - | ||
| 127 | - | ||
| 128 | - | ||
| 129 | - | ||
| 130 | - | ||
| 131 | - | ||
| 132 | - | ||
| 133 | - | ||
| 198 | + | 示例: | |
| 199 | + | ``` | |
| 200 | + | Type | |
| 201 | + | TMyThread = class(TThread) | |
| 202 | + | private | |
| 203 | + | fStatusText : string; | |
| 204 | + | procedure ShowStatus; | |
| 205 | + | protected | |
| 206 | + | procedure Execute; override; | |
| 207 | + | public | |
| 208 | + | Constructor Create(CreateSuspended : boolean); | |
| 209 | + | end; | |
| 210 | + | constructor TMyThread.Create(CreateSuspended : boolean); | |
| 134 | 211 | begin | |
| 135 | - | ||
| 136 | - | ||
| 137 | - | ||
| 138 | - | ||
| 212 | + | inherited Create(CreateSuspended); | |
| 213 | + | FreeOnTerminate := True; | |
| 214 | + | end; | |
| 215 | + | ||
| 216 | + | procedure TMyThread.ShowStatus; | |
| 217 | + | // 本方法由主线程执行,因此可以访问所有 GUI 元素。 | |
| 218 | + | begin | |
| 219 | + | Form1.Caption := fStatusText; | |
| 220 | + | end; | |
| 221 | + | ||
| 222 | + | procedure TMyThread.Execute; | |
| 223 | + | var | |
| 224 | + | newStatus : string; | |
| 225 | + | begin | |
| 226 | + | fStatusText := 'TMyThread Starting...'; | |
| 227 | + | Synchronize(@Showstatus); | |
| 228 | + | fStatusText := 'TMyThread Running...'; | |
| 229 | + | while (not Terminated) and ([其他条件]) do | |
| 139 | 230 | begin | |
| 140 | - | ||
| 141 | - | ||
| 231 | + | ... | |
| 232 | + | [here goes the code of the main thread loop] | |
| 233 | + | ... | |
| 234 | + | if NewStatus <> fStatusText then | |
| 235 | + | begin | |
| 236 | + | fStatusText := newStatus; | |
| 237 | + | Synchronize(@Showstatus); | |
| 238 | + | end; | |
| 142 | 239 | end; | |
| 143 | 240 | end; | |
| 144 | - | ||
| 145 | 241 | ``` | |
| 146 | 242 | ||
| 147 | - | ||
| 148 | - | ||
| 149 | - | ||
| 150 | - | ||
| 151 | - | ||
| 152 | - | ||
| 153 | - | ||
| 154 | - | ||
| 155 | - | ||
| 156 | - | ||
| 157 | - | ||
| 158 | - | ||
| 243 | + | 在应用程序中, | |
| 159 | 244 | ``` | |
| 160 | - | ||
| 161 | - | ||
| 162 | - | ||
| 163 | - | ||
| 164 | - | ||
| 165 | - | ||
| 166 | - | ||
| 167 | - | ||
| 168 | - | ||
| 169 | - | ||
| 170 | - | ||
| 171 | - | ||
| 172 | - | ||
| 173 | - | ||
| 174 | - | ||
| 175 | - | ||
| 176 | - | ||
| 177 | - | ||
| 178 | - | ||
| 179 | - | ||
| 180 | - | ||
| 181 | - | ||
| 182 | - | ||
| 183 | - | ||
| 184 | - | ||
| 185 | - | ||
| 186 | - | ||
| 187 | - | ||
| 188 | - | ||
| 189 | - | ||
| 190 | - | ||
| 191 | - | ||
| 192 | - | ||
| 193 | - | ||
| 194 | - | ||
| 195 | - | ||
| 196 | - | ||
| 197 | - | ||
| 198 | - | ||
| 199 | - | ||
| 200 | - | ||
| 201 | - | ||
| 202 | - | ||
| 203 | - | ||
| 204 | - | ||
| 205 | - | ||
| 245 | + | var | |
| 246 | + | MyThread : TMyThread; | |
| 206 | 247 | begin | |
| 248 | + | MyThread := TMyThread.Create(True); // 这样线程就不会自动启动 | |
| 207 | 249 | ... | |
| 208 | - | ||
| 250 | + | [此处放置线程开始执行前的初始化代码] | |
| 209 | 251 | ... | |
| 210 | - | ||
| 252 | + | MyThread.Start; | |
| 253 | + | end; | |
| 254 | + | ``` | |
| 255 | + | ||
| 256 | + | 若要应用更加灵活,可为线程创建一个事件;这样同步方法就不会与哪个窗体或类绑在一起:可在线程事件上绑定监听器。下面给出一个示例: | |
| 257 | + | ``` | |
| 258 | + | Type | |
| 259 | + | TShowStatusEvent = procedure(Status: String) of Object; | |
| 260 | + | ||
| 261 | + | TMyThread = class(TThread) | |
| 262 | + | private | |
| 263 | + | fStatusText : string; | |
| 264 | + | FOnShowStatus: TShowStatusEvent; | |
| 265 | + | procedure ShowStatus; | |
| 266 | + | protected | |
| 267 | + | procedure Execute; override; | |
| 268 | + | public | |
| 269 | + | Constructor Create(CreateSuspended : boolean); | |
| 270 | + | property OnShowStatus: TShowStatusEvent read FOnShowStatus write FOnShowStatus; | |
| 271 | + | end; | |
| 272 | + | ||
| 273 | + | constructor TMyThread.Create(CreateSuspended : boolean); | |
| 274 | + | begin | |
| 275 | + | inherited Create(CreateSuspended); | |
| 276 | + | FreeOnTerminate := True; | |
| 277 | + | end; | |
| 278 | + | ||
| 279 | + | procedure TMyThread.ShowStatus; | |
| 280 | + | // 本方法由主线程执行,因此可以访问所有 GUI 元素。 | |
| 281 | + | begin | |
| 282 | + | if Assigned(FOnShowStatus) then | |
| 283 | + | begin | |
| 284 | + | FOnShowStatus(fStatusText); | |
| 285 | + | end; | |
| 286 | + | end; | |
| 287 | + | ||
| 288 | + | procedure TMyThread.Execute; | |
| 289 | + | var | |
| 290 | + | newStatus : string; | |
| 291 | + | begin | |
| 292 | + | fStatusText := 'TMyThread Starting...'; | |
| 293 | + | Synchronize(@Showstatus); | |
| 294 | + | fStatusText := 'TMyThread Running...'; | |
| 295 | + | while (not Terminated) and ([any condition required]) do | |
| 211 | 296 | begin | |
| 212 | - | ||
| 213 | - | ||
| 297 | + | ... | |
| 298 | + | [here goes the code of the main thread loop] | |
| 299 | + | ... | |
| 300 | + | if NewStatus <> fStatusText then | |
| 301 | + | begin | |
| 302 | + | fStatusText := newStatus; | |
| 303 | + | Synchronize(@Showstatus); | |
| 304 | + | end; | |
| 214 | 305 | end; | |
| 215 | 306 | end; | |
| 216 | - | ||
| 217 | 307 | ``` | |
| 218 | 308 | ||
| 219 | - | ||
| 220 | - | ||
| 221 | - | ||
| 222 | - | ||
| 223 | - | ||
| 224 | - | ||
| 225 | - | ||
| 226 | - | ||
| 227 | - | ||
| 228 | - | ||
| 229 | - | ||
| 230 | - | ||
| 231 | - | ||
| 232 | - | ||
| 233 | - | ||
| 234 | - | ||
| 235 | - | ||
| 236 | - | ||
| 237 | - | ||
| 238 | - | ||
| 239 | - | ||
| 240 | - | ||
| 241 | - | ||
| 242 | - | ||
| 243 | - | ||
| 244 | - | ||
| 245 | - | ||
| 246 | - | ||
| 247 | - | ||
| 248 | - | ||
| 249 | - | ||
| 250 | - | ||
| 251 | - | ||
| 252 | - | ||
| 253 | - | ||
| 254 | - | ||
| 255 | - | ||
| 256 | - | ||
| 257 | - | ||
| 258 | - | ||
| 259 | - | ||
| 260 | - | ||
| 309 | + | 应用程序中的代码: | |
| 261 | 310 | ``` | |
| 262 | - | ||
| 263 | - | ||
| 264 | - | ||
| 265 | - | ||
| 266 | - | ||
| 267 | - | ||
| 311 | + | Type | |
| 312 | + | TForm1 = class(TForm) | |
| 313 | + | Button1: TButton; | |
| 314 | + | Label1: TLabel; | |
| 315 | + | procedure FormCreate(Sender: TObject); | |
| 316 | + | procedure FormDestroy(Sender: TObject); | |
| 317 | + | private | |
| 318 | + | { private declarations } | |
| 319 | + | MyThread: TMyThread; | |
| 320 | + | procedure ShowStatus(Status: string); | |
| 321 | + | public | |
| 322 | + | { public declarations } | |
| 323 | + | end; | |
| 324 | + | ||
| 325 | + | procedure TForm1.FormCreate(Sender: TObject); | |
| 326 | + | begin | |
| 327 | + | inherited; | |
| 328 | + | MyThread := TMyThread.Create(true); | |
| 329 | + | MyThread.OnShowStatus := @ShowStatus; | |
| 330 | + | end; | |
| 331 | + | ||
| 332 | + | procedure TForm1.FormDestroy(Sender: TObject); | |
| 333 | + | begin | |
| 334 | + | MyThread.Terminate; | |
| 335 | + | ||
| 336 | + | // FreeOnTerminate 为 true,因此不应再放置释放代码: | |
| 337 | + | // MyThread.Free; | |
| 338 | + | inherited; | |
| 339 | + | end; | |
| 340 | + | ||
| 341 | + | procedure TForm1.Button1Click(Sender: TObject); | |
| 342 | + | begin | |
| 343 | + | MyThread.Start; | |
| 344 | + | end; | |
| 345 | + | ||
| 346 | + | procedure TForm1.ShowStatus(Status: string); | |
| 347 | + | begin | |
| 348 | + | Label1.Caption := Status; | |
| 349 | + | end; | |
| 350 | + | ``` | |
| 268 | 351 | ||
| 269 | - | ||
| 352 | + | ## 特别注意事项 | |
| 270 | 353 | ||
| 271 | - | ||
| 354 | + | ### Windows 栈检查 | |
| 272 | 355 | ||
| 273 | - | ||
| 356 | + | 在 Windows 中使用线程时,如果启用了 -Ct(栈检查)开关,可能会遇到一个令人头疼的问题。原因尚不明,如果用了默认堆栈大小,那么在每次调用 TThread.Create 时都会“触发”栈检查。目前唯一的变通办法就是不启用 -Ct 开关。请注意,异常不是在主线程中触发,而是在新建的线程中。于是会造成线程就像是未曾启动。 | |
| 274 | 357 | ||
| 275 | - | ||
| 358 | + | 以下是一段良好的代码,用于检查这种情况以及线程创建过程中可能发生的其他异常: | |
| 359 | + | ``` | |
| 276 | 360 | MyThread := TThread.Create(False); | |
| 277 | - | ||
| 278 | - | ||
| 361 | + | ||
| 362 | + | if Assigned(MyThread.FatalException) then | |
| 363 | + | raise MyThread.FatalException; | |
| 279 | 364 | ``` | |
| 280 | 365 | ||
| 281 | - | ||
| 366 | + | 上述代码确保线程创建过程中的所有异常都会在主线程中触发。 | |
| 282 | 367 | ||
| 283 | - | ||
| 368 | + | ### Windows 的 GetDC 函数 | |
| 284 | 369 | ||
| 285 | - | ||
| 370 | + | GetDC(及 GetDCEx)函数用于检索指定窗口区域或整个屏幕的设备上下文(DC)句柄。比如可用于屏幕截图。但请注意,GetDC 不是线程安全函数:根据文档说明,设备上下文句柄任何时候只能由一个线程使用。 | |
| 286 | 371 | ||
| 287 | - | ||
| 372 | + | ### 多线程软件包 | |
| 288 | 373 | ||
| 289 | - | ||
| 374 | + | 用到多线程的软件包,应在自定义选项中添加 -dUseCThreads 标志。打开包编辑器,然后选择“选项>用法(Usage)>自定义”,加入 -dUseCThreads。这样将为使用此软件包的所有项目和包(包括 IDE)定义此标志。IDE 及其创建的所有新应用已在 .lpr 文件中包含了以下代码: | |
| 375 | + | ``` | |
| 290 | 376 | uses | |
| 291 | - | ||
| 292 | - | ||
| 293 | - | ||
| 294 | - | ||
| 377 | + | {$IFDEF UNIX}{$IFDEF UseCThreads} | |
| 378 | + | cthreads, | |
| 379 | + | cmem, // the c memory manager is on some systems much faster for multi-threading | |
| 380 | + | {$ENDIF}{$ENDIF} | |
| 295 | 381 | ``` | |
| 296 | 382 | ||
| 297 | 383 | ### Heaptrc | |
| 298 | 384 | ||
| 299 | - | ||
| 300 | - | ||
| 301 | - | ||
| 302 | - | ||
| 303 | - | ||
| 385 | + | 在用到 cmem 单元时不能启用 -gh 开关。-gh 开关会用到 heaptrc 单元,而 heaptrc 单元对堆管理器的代码做了扩充。因此,在引用 **cmem** 单元时,**heaptrc** 单元必须加在**后面** 。 | |
| 386 | + | ``` | |
| 304 | 387 | uses | |
| 305 | - | ||
| 306 | - | ||
| 307 | - | ||
| 308 | - | ||
| 309 | - | ||
| 388 | + | {$IFDEF UNIX}{$IFDEF UseCThreads} | |
| 389 | + | cthreads, | |
| 390 | + | cmem, // the c memory manager is on some systems much faster for multi-threading | |
| 391 | + | {$ENDIF}{$ENDIF} | |
| 392 | + | heaptrc, | |
| 310 | 393 | ``` | |
| 311 | 394 | ||
| 312 | - | ||
| 395 | + | ### 线程的初始化和终止 | |
| 313 | 396 | ||
| 314 | - | ||
| 397 | + | 若要对线程对象本身进行初始化,可以用挂起状态启动并设置属性,也可以新建一个构造函数并调用继承的构造函数。 | |
| 315 | 398 | ||
| 316 | - | ||
| 399 | + | **注意** :如 CreateSuspended=false,则使用 AfterConstruction 是很危险的,因为线程已经启动了。 | |
| 317 | 400 | ||
| 318 | - | ||
| 401 | + | 析构函数则可用于释放对象所占资源。 | |
| 402 | + | ``` | |
| 403 | + | type | |
| 404 | + | TMyThread = class(TThread) | |
| 405 | + | private | |
| 406 | + | fRTLEvent: PRTLEvent; | |
| 407 | + | public | |
| 408 | + | constructor Create(SomeData: TSomeObject; CreateSuspended: boolean); | |
| 409 | + | destructor Destroy; override; | |
| 410 | + | end; | |
| 411 | + | ||
| 412 | + | constructor TMyThread.Create(SomeData: TSomeObject; CreateSuspended: boolean); | |
| 413 | + | begin | |
| 414 | + | // 示例:设置事件、临界区、其他文件或数据库连接之类的资源 | |
| 415 | + | RTLEventCreate(fRTLEvent); | |
| 416 | + | inherited Create(CreateSuspended); | |
| 417 | + | end; | |
| 418 | + | ||
| 419 | + | destructor TMyThread.Destroy; | |
| 420 | + | begin | |
| 421 | + | RTLeventDestroy(fRTLEvent); | |
| 422 | + | inherited Destroy; | |
| 423 | + | end; | |
| 424 | + | ``` | |
| 319 | 425 | ||
| 320 | - | ||
| 426 | + | ### 非 LCL 程序 | |
| 321 | 427 | ||
| 322 | - | ||
| 428 | + | TThread.Synchronize 需要主线程定期调用 CheckSynchronize。LCL 会在事件循环中进行调用。如果不使用 LCL 的事件循环,那就必须自行完成调用。 | |
| 323 | 429 | ||
| 324 | - | ||
| 430 | + | ## SMP支持 | |
| 325 | 431 | ||
| 326 | - | ||
| 432 | + | 好消息是,如果你的应用程序在多线程上工作正常,看来SMP已经启用。 | |
| 327 | 433 | ||
| 328 | - | ||
| 434 | + | ## Lazarus 调试多线程应用 | |
| 329 | 435 | ||
| 330 | - | ||
| 436 | + | Lazarus 的调试需要 GDB,并且其功能正在迅速变得更加全面和稳定。但仍有一些 Linux 版本存在问题。 | |
| 331 | 437 | ||
| 332 | - | ||
| 333 | - | ||
| 334 | - | ||
| 335 | - | ||
| 336 | - | ||
| 337 | - | ||
| 438 | + | ### 调试时的输出 | |
| 338 | 439 | ||
| 339 | - | ||
| 440 | + | 在单线程应用中,写入控制台/终端/其他输出设备都很简单,并且输出顺序与写入顺序相同。但多线程应用中的情况要复杂得多。如果两个线程同时写入,比如线程A先写一行,线程B再写一行,这两行不一定会按这个顺序写入。甚至可能一个线程在输出时,另一个线程也在写入。在 Linux 下,DebugLn() 输出(可能)会得到正确顺序 。但在 Win32 下,由于是在主线程之外使用 DebugLn(),可能会引发异常(可能是 DiskFull)。因此,为了避免麻烦,请使用以下 DebugLnThreadLog()。 | |
| 340 | 441 | ||
| 341 | - | ||
| 342 | - | ||
| 442 | + | LCLProc 单元中有几个函数,可供每个线程写入自己的日志文件: | |
| 343 | 443 | ``` | |
| 444 | + | procedure DbgOutThreadLog(const Msg: string); overload; | |
| 445 | + | procedure DebuglnThreadLog(const Msg: string); overload; | |
| 446 | + | procedure DebuglnThreadLog(Args: array of const); overload; | |
| 447 | + | procedure DebuglnThreadLog; overload; | |
| 448 | + | ``` | |
| 344 | 449 | ||
| 345 | - | ||
| 450 | + | 比如,不用 _writeln('Some text ',123);_ 而是用 | |
| 451 | + | ``` | |
| 452 | + | DebuglnThreadLog(['Some text ',123]); | |
| 453 | + | ``` | |
| 346 | 454 | ||
| 347 | - | ||
| 455 | + | 这样就会在 **Log <PID>.txt** 文件中追加一行 'Some text 123',其中 <PID> 是当前线程的进程 ID。 | |
| 348 | 456 | ||
| 349 | - | ||
| 350 | - | ||
| 457 | + | 在每次运行线程前删除日志文件,是个好办法: | |
| 351 | 458 | ``` | |
| 459 | + | rm -f Log* && ./project1 | |
| 460 | + | ``` | |
| 352 | 461 | ||
| 353 | 462 | ### Linux | |
| 354 | 463 | ||
| 355 | - | ||
| 356 | - | ||
| 357 | - | ||
| 358 | - | ||
| 359 | - | ||
| 360 | - | ||
| 361 | - | ||
| 362 | - | ||
| 363 | - | ||
| 364 | - | ||
| 365 | - | ||
| 464 | + | 若要在 Linux 中调试多线程应用,会碰到一个大问题:X Windows 的桌面管理器可能会**挂起** 。比如应用捕获了鼠标/键盘并被 gdb 暂停时就会发生这种情况,此时 X 系统会等待应用响应。这时只要从另一台计算机登录并杀掉 gdb 即可,或者按 CTRL+ALT+F3 退出会话并杀掉 gdb。另外也可以重启窗口管理器:输入 sudo /etc/init.d/gdm restart,会重新回到桌面。 | |
| 366 | 465 | ||
| 367 | - | ||
| 368 | - | ||
| 466 | + | gdb 停止应用程序的时机不一定每次都一样,因此有些技巧还是有用的:Ubuntu x64 系统下可设置一下项目的选项,以便获得调试所需的额外信息文件…… | |
| 369 | 467 | ``` | |
| 370 | - | ||
| 371 | - | ||
| 372 | - | ||
| 373 | - | ||
| 374 | - | ||
| 468 | + | 项目选项->编译器选项->调试,勾选“使用外部调试符号文件(-Xg)”。 | |
| 375 | 469 | ``` | |
| 376 | 470 | ||
| 377 | - | ||
| 378 | - | ||
| 379 | - | ||
| 380 | - | ||
| 381 | - | ||
| 382 | - | ||
| 383 | - | ||
| 384 | - | ||
| 385 | - | ||
| 386 | - | ||
| 471 | + | 另一种做法是再开一个X桌面,一个桌面运行 IDE/gdb,另一个桌面运行应用程序,这样只会挂起调试的桌面。用以下命令新建 X 实例: | |
| 387 | 472 | ``` | |
| 388 | - | ||
| 389 | - | ||
| 473 | + | X :1 & | |
| 474 | + | ``` | |
| 390 | 475 | ||
| 391 | - | ||
| 476 | + | 这样就会新开一个桌面,切换到另一个桌面(比如按 CTRL+ALT+F7)时,就能按 CTRL+ALT+F8 返回新开的桌面(如果组合键不起作用,尝试按CTRL+ALT+F2 …… 在 [Slackware](<http://www.slackware.com>) 中是有效的)。 | |
| 392 | 477 | ||
| 393 | - | ||
| 478 | + | 然后即可创建桌面会话如下: | |
| 479 | + | ``` | |
| 480 | + | gnome-session --display=:1 & | |
| 481 | + | ``` | |
| 394 | 482 | ||
| 395 | - | ||
| 483 | + | 然后,在 Lazarus 中,在项目的“运行参数”对话框中,勾选“使用显示”并输入 :1。 | |
| 396 | 484 | ||
| 397 | - | ||
| 485 | + | 应用将运行于第二个 X 服务上,且可在第一个 X 服务上进行调试。 | |
| 398 | 486 | ||
| 399 | - | ||
| 487 | + | 已在 Windows 和 Linux 中 Free Pascal 2.0 和 Lazarus 0.9.10 通过测试。 | |
| 400 | 488 | ||
| 401 | - | ||
| 489 | + | * * * | |
| 402 | 490 | ||
| 403 | - | ||
| 491 | + | 不新开 X 会话的话,还可以使用 [Xnest](<http://en.wikipedia.org/wiki/Xnest>)。Xnest 是运行于窗口上的 X 会话。调试线程时使用了 Xnest,X 服务就不会锁住,而且不用来回切换终端,调试起来更加容易。 | |
| 404 | 492 | ||
| 405 | - | ||
| 406 | - | ||
| 493 | + | 运行 Xnest 的命令行: | |
| 407 | 494 | ``` | |
| 408 | - | ||
| 409 | - | ||
| 410 | - | ||
| 411 | - | ||
| 495 | + | Xnest :1 -ac | |
| 496 | + | ``` | |
| 412 | 497 | ||
| 413 | - | ||
| 498 | + | 以上将在:1创建一个 X 会话,并禁用访问控制。 | |
| 414 | 499 | ||
| 415 | - | ||
| 500 | + | ## Lazarus 界面组件的接口 | |
| 416 | 501 | ||
| 417 | - | ||
| 502 | + | Win32、gtk 和 carbon 接口都支持多线程,就是说 TThread、临界区和 Synchronize 都能用。但这些接口并不是线程安全的,就是说一次只能有一个线程访问 LCL。并且由于主线程永远不允许等待其他线程,就是说只有主线程才能访问 LCL,包括所有与 TControl、Application 和 LCL 控件句柄相关的操作。LCL 中有一些函数是线程安全的,比如 FileUtil 单元中的大多数函数。 | |
| 418 | 503 | ||
| 419 | - | ||
| 504 | + | ### 利用 SendMessage/PostMessage 进行线程间通信 | |
| 420 | 505 | ||
| 421 | - | ||
| 506 | + | 一个应用应该只有一个线程(通常是主线程)会去调用 LCL API。其他线程可以通过多种间接方法使用 LCL,其中一种不错的做法就是利用 SendMessage 或 PostMessage。LCLIntf.SendMessage 和 LCLIntf.PostMessage 会一条带有窗口指向的消息送入应用的消息池。 | |
| 422 | 507 | ||
| 423 | - | ||
| 424 | - | ||
| 508 | + | 关于这两个函数,请参看以下文档: | |
| 425 | 509 | ||
| 426 | - | ||
| 510 | + | * [SendMessage](<http://lazarus-ccr.sourceforge.net/docs/lcl/lclintf/sendmessage.html> "doc:lcl/lclintf/sendmessage.html") | |
| 511 | + | * [PostMessage](<http://lazarus-ccr.sourceforge.net/docs/lcl/lclintf/postmessage.html> "doc:lcl/lclintf/postmessage.html") | |
| 427 | 512 | ||
| 428 | - | ||
| 513 | + | SendMessage 和 PostMessage 的区别在于,将控制权交回调用线程的方式。与 Synchronize 类似,SendMessage 会阻塞调用线程,直到目标窗口完成消息处理后才返回控制权;但在某些情况下,SendMessage 可能需要待在调用线程的上下文中进行优化操作。而 PostMessage 则会立即返回控制权,只要排队的消息数量不超过系统定义的最大值,并且堆上仍有填加数据的空间。 | |
| 429 | 514 | ||
| 430 | - | ||
| 515 | + | 无论采用哪种发送方式,在消息处理函数中(见下文)都应避免调用 application.ProcessMessages,因为可能会导致再发送一次消息,消息将递归处理。如果无法避免,那么最好采用其他机制在线程之间传递串行事件。 | |
| 431 | 516 | ||
| 432 | - | ||
| 517 | + | 以下是一个示例,展示了辅助线程如何将文本发给主线程,显示于 LCL 控件中: | |
| 518 | + | ``` | |
| 433 | 519 | const | |
| 434 | - | ||
| 435 | - | ||
| 436 | - | ||
| 437 | - | ||
| 438 | - | ||
| 439 | - | ||
| 440 | - | ||
| 441 | - | ||
| 442 | - | ||
| 443 | - | ||
| 444 | - | ||
| 445 | - | ||
| 520 | + | WM_GOT_ERROR = LM_USER + 2004; | |
| 521 | + | WM_VERBOSE = LM_USER + 2005; | |
| 522 | + | ||
| 523 | + | procedure VerboseLog(Msg: string); | |
| 524 | + | var | |
| 525 | + | PError: PChar; | |
| 526 | + | begin | |
| 527 | + | if MessageHandler = 0 then Exit; | |
| 528 | + | PError := StrAlloc(Length(Msg)+1); | |
| 529 | + | StrCopy(PError, PChar(Msg)); | |
| 530 | + | PostMessage(formConsole.Handle, WM_VERBOSE, Integer(PError), 0); | |
| 531 | + | end; | |
| 446 | 532 | ``` | |
| 447 | 533 | ||
| 448 | - | ||
| 449 | - | ||
| 450 | - | ||
| 534 | + | 下面是处理窗口消息的示例: | |
| 535 | + | ``` | |
| 451 | 536 | const | |
| 452 | - | ||
| 453 | - | ||
| 454 | - | ||
| 455 | - | ||
| 456 | - | ||
| 457 | - | ||
| 458 | - | ||
| 459 | - | ||
| 460 | - | ||
| 461 | - | ||
| 462 | - | ||
| 463 | - | ||
| 464 | - | ||
| 465 | - | ||
| 466 | - | ||
| 467 | - | ||
| 468 | - | ||
| 469 | - | ||
| 470 | - | ||
| 471 | - | ||
| 472 | - | ||
| 473 | - | ||
| 474 | - | ||
| 475 | - | ||
| 476 | - | ||
| 477 | - | ||
| 478 | - | ||
| 479 | - | ||
| 480 | - | ||
| 481 | - | ||
| 482 | - | ||
| 483 | - | ||
| 484 | - | ||
| 485 | - | ||
| 486 | - | ||
| 487 | - | ||
| 488 | - | ||
| 489 | - | ||
| 537 | + | WM_GOT_ERROR = LM_USER + 2004; | |
| 538 | + | WM_VERBOSE = LM_USER + 2005; | |
| 539 | + | ||
| 540 | + | type | |
| 541 | + | { TformConsole } | |
| 542 | + | ||
| 543 | + | TformConsole = class(TForm) | |
| 544 | + | DebugList: TListView; | |
| 545 | + | // ... | |
| 546 | + | private | |
| 547 | + | procedure HandleDebug(var Msg: TLMessage); message WM_VERBOSE; | |
| 548 | + | end; | |
| 549 | + | ||
| 550 | + | var | |
| 551 | + | formConsole: TformConsole; | |
| 552 | + | ||
| 553 | + | implementation | |
| 554 | + | ||
| 555 | + | .... | |
| 556 | + | ||
| 557 | + | { TformConsole } | |
| 558 | + | ||
| 559 | + | procedure TformConsole.HandleDebug(var Msg: TLMessage); | |
| 560 | + | var | |
| 561 | + | Item: TListItem; | |
| 562 | + | MsgStr: PChar; | |
| 563 | + | MsgPasStr: string; | |
| 564 | + | begin | |
| 565 | + | MsgStr := PChar(Msg.wparam); | |
| 566 | + | MsgPasStr := StrPas(MsgStr); | |
| 567 | + | Item := DebugList.Items.Add; | |
| 568 | + | Item.Caption := TimeToStr(SysUtils.Now); | |
| 569 | + | Item.SubItems.Add(MsgPasStr); | |
| 570 | + | Item.MakeVisible(False); | |
| 571 | + | ||
| 572 | + | // 下面可干点事情,比如: | |
| 573 | + | ||
| 574 | + | TrayControl.SetError(MsgPasStr); | |
| 575 | + | StrDispose(MsgStr) | |
| 576 | + | end; | |
| 577 | + | ||
| 578 | + | end. | |
| 490 | 579 | ``` | |
| 491 | 580 | ||
| 492 | - | ||
| 493 | - | ||
| 494 | - | ||
| 581 | + | 如果在 Linux x64 机器上遇到错误:“Project XY raised exception class 'External: SIGSEGV'”时,就得将“VerboseLog”中的“Integer(PError)”修改为“PtrInt(PError)”。修改后的这行代码应如下所示: | |
| 582 | + | ``` | |
| 583 | + | PostMessage(formConsole.Handle, WM_VERBOSE, PtrInt(PError), 0); | |
| 584 | + | ``` | |
| 495 | 585 | ||
| 496 | - | ||
| 586 | + | ## 临界区(critical section) | |
| 497 | 587 | ||
| 498 | - | ||
| 588 | + | _临界区_ 是一个对象,用于确保某部分代码在任何时候只能由一个线程执行。临界区在使用前需要创建/初始化,在不用时需释放。 | |
| 499 | 589 | ||
| 500 | - | ||
| 590 | + | 临界区有两种使用方式,RTL 或 LCL。以下给出 RTL 的用法,平替的 LCL 方式则以注释的形式给出,唯一区别就是方法名不同。 | |
| 501 | 591 | ||
| 502 | - | ||
| 503 | - | ||
| 592 | + | 使用临界区时通常如下引入: | |
| 504 | 593 | ``` | |
| 594 | + | // uses LCLIntf, LCLType; | |
| 595 | + | ``` | |
| 505 | 596 | ||
| 506 | - | ||
| 597 | + | 所需的 RTL 单元几乎都可涵盖。 | |
| 507 | 598 | ||
| 508 | - | ||
| 509 | - | ||
| 599 | + | 声明(对所有要访问临界区的线程而言,是全局变量): | |
| 510 | 600 | ``` | |
| 601 | + | MyCriticalSection: TRTLCriticalSection; | |
| 602 | + | // MyCriticalSection : TCriticalSection; | |
| 603 | + | ``` | |
| 511 | 604 | ||
| 512 | - | ||
| 605 | + | 创建: | |
| 606 | + | ``` | |
| 607 | + | InitCriticalSection(MyCriticalSection); | |
| 608 | + | //InitializeCriticalSection(MyCriticalSection); | |
| 609 | + | ``` | |
| 513 | 610 | ||
| 514 | - | ||
| 611 | + | 运行一些线程。独占地执行某些操作: | |
| 612 | + | ``` | |
| 515 | 613 | EnterCriticalSection(MyCriticalSection); | |
| 516 | - | ||
| 517 | - | ||
| 518 | - | ||
| 519 | - | ||
| 520 | - | ||
| 614 | + | try | |
| 615 | + | // 访问变量、写入文件、发送网络包等 | |
| 616 | + | finally | |
| 617 | + | LeaveCriticalSection(MyCriticalSection); | |
| 618 | + | end; | |
| 619 | + | // RTL 和 LCL 的语法相同,不错吧。 | |
| 521 | 620 | ``` | |
| 522 | 621 | ||
| 523 | - | ||
| 524 | - | ||
| 525 | - | ||
| 526 | - | ||
| 622 | + | 所有线程结束后,释放临界区: | |
| 527 | 623 | ``` | |
| 528 | - | ||
| 529 | - | ||
| 624 | + | DoneCriticalSection(MyCriticalSection); | |
| 625 | + | // DeleteCriticalSection(MyCriticalSection); | |
| 626 | + | ``` | |
| 530 | 627 | ||
| 531 | - | ||
| 628 | + | 另一种方案是使用 TCriticalSection 对象。对象创建即初始化,Enter 方法执行 EnterCriticalSection 操作,Leave 方法执行 LeaveCriticalSection 操作,而对象销毁则执行删除操作。 | |
| 532 | 629 | ||
| 533 | - | ||
| 630 | + | 请注意,临界区无法防止同一线程多次进入同一块代码,而只能防止不同线程同时进入。因此,临界区不能用于防止消息处理函数的重入(请参阅前文)。 | |
| 534 | 631 | ||
| 535 | - | ||
| 632 | + | 示例:5个线程同时递增1个计数器。 请参阅 lazarus/examples/multithreading/criticalsectionexample1.lpi。 | |
| 536 | 633 | ||
| 537 | - | ||
| 634 | + | [](</File:Warning-icon.png>) | |
| 538 | 635 | ||
| 539 | - | ||
| 636 | + | **Warning:** 上述4个函数分成了两组,分别由 RTL 和 LCL 提供。LCL 的函数在 LCLIntf 和 LCLType 单元中定义。这两组函数的工作方式大致相同,应用程序中可以同时使用,但不得在 LCL 临界区内使用 RTL 函数,反之亦然。 | |
| 540 | 637 | ||
| 541 | - | ||
| 638 | + | ### 共享变量 | |
| 542 | 639 | ||
| 543 | - | ||
| 640 | + | 如果有多个线程共享一个只读变量,那没什么可担心的。直接读就是了。 但如果有一个或多个线程要修改变量值,那就必须确保每次只有一个线程能够访问。 | |
| 544 | 641 | ||
| 545 | - | ||
| 642 | + | 示例:5个线程递增1个计数器。 参见 lazarus/examples/multithreading/criticalsectionexample1.lpi | |
| 546 | 643 | ||
| 547 | - | ||
| 644 | + | ## 等待其他线程 | |
| 548 | 645 | ||
| 549 | - | ||
| 646 | + | 如果线程A需要用到线程B的运行结果,那就必须等待至线程B完成。 | |
| 550 | 647 | ||
| 551 | - | ||
| 648 | + | **重要:** 主线程绝不能等待其他线程,而应使用 Synchronize 方法(参见前文)。 | |
| 552 | 649 | ||
| 553 | - | ||
| 650 | + | 示例参见:lazarus/examples/multithreading/waitforexample1.lpi | |
| 651 | + | ``` | |
| 554 | 652 | { TThreadA } | |
| 555 | - | ||
| 556 | - | ||
| 557 | - | ||
| 558 | - | ||
| 559 | - | ||
| 560 | - | ||
| 561 | - | ||
| 562 | - | ||
| 563 | - | ||
| 564 | - | ||
| 565 | - | ||
| 566 | - | ||
| 567 | - | ||
| 568 | - | ||
| 569 | - | ||
| 570 | - | ||
| 571 | - | ||
| 572 | - | ||
| 573 | - | ||
| 574 | - | ||
| 575 | - | ||
| 576 | - | ||
| 577 | - | ||
| 578 | - | ||
| 579 | - | ||
| 580 | - | ||
| 581 | - | ||
| 582 | - | ||
| 653 | + | ||
| 654 | + | procedure TThreadA.Execute; | |
| 655 | + | begin | |
| 656 | + | Form1.ThreadB:=TThreadB.Create(false); | |
| 657 | + | // 创建事件 | |
| 658 | + | WaitForB:=RTLEventCreate; | |
| 659 | + | while not Application.Terminated do begin | |
| 660 | + | // 一直等待,直至线程B唤醒A | |
| 661 | + | RtlEventWaitFor(WaitForB); | |
| 662 | + | writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter)); | |
| 663 | + | end; | |
| 664 | + | end; | |
| 665 | + | ||
| 666 | + | { TThreadB } | |
| 667 | + | ||
| 668 | + | procedure TThreadB.Execute; | |
| 669 | + | var | |
| 670 | + | i: Integer; | |
| 671 | + | begin | |
| 672 | + | Counter:=0; | |
| 673 | + | while not Application.Terminated do begin | |
| 674 | + | // 线程 B 执行任务 | |
| 675 | + | Sleep(1500); | |
| 676 | + | inc(Counter); | |
| 677 | + | // 唤醒线程 A | |
| 678 | + | RtlEventSetEvent(Form1.ThreadA.WaitForB); | |
| 679 | + | end; | |
| 680 | + | end; | |
| 583 | 681 | ``` | |
| 584 | 682 | ||
| 585 | - | ||
| 586 | - | ||
| 587 | - | ||
| 683 | + |  **Note:** RtlEventSetEvent可在 RtlEventWaitFor 之前调用。RtlEventWaitFor 将立即返回。可用 RTLeventResetEvent 清除标志。 | |
| 588 | 684 | ||
| 589 | 685 | ## Fork | |
| 590 | 686 | ||
| 591 | - | ||
| 687 | + | 多线程应用在创建子进程时,请注意在 fork(或 fpFork)调用**之前** 创建并运行的所有线程均不会在子进程中运行。正如 fork() 手册所述,任何在 fork 调用之前运行的线程,状态将是未定义。 | |
| 592 | 688 | ||
| 593 | - | ||
| 689 | + | 因此,请注意在 fork 调用之前初始化的任何线程(包括 initialization 部分中的线程)都**不会** 工作。 | |
| 594 | 690 | ||
| 595 | - | ||
| 691 | + | ## 并行过程/循环 | |
| 596 | 692 | ||
| 597 | - | ||
| 693 | + | 多线程的一个特例是并行运行单个过程。请参见[[Parallel procedures|并行过程]]。 | |
| 598 | 694 | ||
| 599 | 695 | ## 分布式计算 | |
| 600 | 696 | ||
| 601 | - | ||
| 697 | + | 多线程的更高境界是在多台机器上运行这些线程。 | |
| 602 | 698 | ||
| 603 | - | ||
| 604 | - | ||
| 699 | + | * 可以使用 synapse、lnet、indy 之类的 TCP 组件进行通信,以提供最大的灵活性,通常用于松散连接的客户端/服务器应用。 | |
| 700 | + | * 可以使用 [[MPICH]] 这种消息传递库,用于在服务器集群上实现 HPC(高性能计算)。 | |
| 605 | 701 | ||
| 606 | 702 | ## 外部线程 | |
| 607 | 703 | ||
| 608 | - | ||
| 609 | - | ||
| 610 | - | ||
| 704 | + | 为了让 Free Pascal 的线程系统能够正常运转,新建 FPC 线程都需要进行初始化(更准确地说,每个线程的异常、I/O系统和 threadvar 系统都需要初始化,以确保本地变量和内存堆能正常工作)。如果使用了 BeginThread(或通过 TThread 类间接使用了),那么这些初始化工作都会自动完成。但如果用到的线程不是由 BeginThread 创建的(即外部线程),则(目前)可能需要多做一些初始化的工作。外部线程还包括在外部 C 语言库(.DLL/.so)中创建的线程。 | |
| 611 | 705 | ||
| 612 | - | ||
| 706 | + | 以下是使用外部线程时需要考虑的事项(可能并非所有版本编译器都需要考虑): | |
| 613 | 707 | ||
| 614 | - | ||
| 708 | + | * 完全不使用外部线程,而应该采用 FPC 线程。如果能控制线程的创建方式,请通过 BeginThread 自行创建线程。 | |
| 615 | 709 | ||
| 616 | - | ||
| 710 | + | 如果调用约定不合适(比如原线程函数遵守 cdecl 调用方式,而 BeginThread 是 pascal 调用方式),则可以创建一个记录,存入需调用的原线程函数,并在 pascal 线程函数中调用,如下所示: | |
| 711 | + | ``` | |
| 617 | 712 | type | |
| 618 | - | ||
| 713 | + | TCdeclThreadFunc = function (user_data:Pointer):Pointer;cdecl; | |
| 714 | + | ||
| 715 | + | PCdeclThreadFuncData = ^TCdeclThreadFuncData; | |
| 716 | + | TCdeclThreadFuncData = record | |
| 717 | + | Func: TCdeclThreadFunc; //cdecl 函数 | |
| 718 | + | Data: Pointer; //原数据 | |
| 719 | + | end; | |
| 720 | + | ||
| 721 | + | // Pascal 线程调用 cdecl 函数 | |
| 722 | + | function C2P_Translator(FuncData: pointer) : ptrint; | |
| 723 | + | var | |
| 724 | + | ThreadData: TCdeclThreadFuncData; | |
| 725 | + | begin | |
| 726 | + | ThreadData := PCdeclThreadFuncData(FuncData)^; | |
| 727 | + | Result := ptrint(ThreadData.Func(ThreadData.Data)); | |
| 728 | + | end; | |
| 729 | + | ||
| 730 | + | procedure CreatePascalThread; | |
| 731 | + | var | |
| 732 | + | ThreadData: PCdeclThreadFuncData; | |
| 733 | + | begin | |
| 734 | + | New(ThreadData); | |
| 735 | + | // 下面是需调用的 cdecl 线程函数 | |
| 736 | + | ThreadData^.Func := func; | |
| 737 | + | ThreadData^.Data := user_data; | |
| 738 | + | // 创建 Pascal 线程 | |
| 739 | + | BeginThread(@C2P_Translator, ThreadData ); | |
| 740 | + | end; | |
| 741 | + | ``` | |
| 619 | 742 | ||
| 620 | - | ||
| 621 | - | ||
| 622 | - | ||
| 623 | - | ||
| 624 | - | ||
| 743 | + | ||
| 625 | 744 | ||
| 626 | - | ||
| 627 | - | ||
| 628 | - | ||
| 629 | - | ||
| 630 | - | ||
| 631 | - | ||
| 632 | - | ||
| 633 | - | ||
| 745 | + | * 通过创建一个虚拟线程来初始化 FPC 的线程系统。如果应用中未创建任何 Pascal 线程,那么线程系统将不会初始化(因此,本地变量和内存堆都不会正常工作)。 | |
| 634 | 746 | ||
| 635 | - | ||
| 636 | - | ||
| 637 | - | ||
| 638 | - | ||
| 639 | - | ||
| 640 | - | ||
| 641 | - | ||
| 642 | - | ||
| 643 | - | ||
| 644 | - | ||
| 645 | - | ||
| 646 | 747 | ``` | |
| 647 | - | ||
| 648 | - | ||
| 649 | - | ||
| 650 | - | ||
| 651 | 748 | type | |
| 652 | - | ||
| 653 | - | ||
| 654 | - | ||
| 655 | - | ||
| 656 | - | ||
| 657 | - | ||
| 658 | - | ||
| 659 | - | ||
| 660 | - | ||
| 661 | - | ||
| 662 | - | ||
| 663 | - | ||
| 664 | - | ||
| 665 | - | ||
| 666 | - | ||
| 667 | - | ||
| 668 | - | ||
| 669 | - | ||
| 749 | + | tc = class(tthread) | |
| 750 | + | procedure execute;override; | |
| 751 | + | end; | |
| 752 | + | ||
| 753 | + | procedure tc.execute; | |
| 754 | + | begin | |
| 755 | + | end; | |
| 756 | + | ||
| 757 | + | { main program } | |
| 758 | + | begin | |
| 759 | + | { initialise threading system } | |
| 760 | + | with tc.create(false) do | |
| 761 | + | begin | |
| 762 | + | waitfor; | |
| 763 | + | free; | |
| 764 | + | end; | |
| 765 | + | { ... 下面是自己的代码 } | |
| 766 | + | end. | |
| 670 | 767 | ``` | |
| 671 | 768 | ||
| 672 | - | ||
| 769 | + | (线程系统初始化后,运行时可能会将系统变量“IsMultiThread”置为 true,FPC 函数利用此变量执行各种加解锁操作。此变量不得自行改动。) | |
| 673 | 770 | ||
| 674 | - | ||
| 771 | + | * 如果上述方法无效,请在外部线程函数中尝试加入以下代码: | |
| 675 | 772 | ||
| 676 | - | ||
| 773 | + | ``` | |
| 677 | 774 | function ExternalThread(param: Pointer): LongInt; stdcall; | |
| 678 | - | ||
| 679 | - | ||
| 680 | - | ||
| 681 | - | ||
| 682 | - | ||
| 683 | - | ||
| 684 | - | ||
| 685 | - | ||
| 686 | - | ||
| 687 | - | ||
| 688 | - | ||
| 775 | + | var | |
| 776 | + | tm: TThreadManager; | |
| 777 | + | begin | |
| 778 | + | GetThreadManager(tm); | |
| 779 | + | tm.AllocateThreadVars; | |
| 780 | + | InitThread(1000000); // 在此调整初始栈的大小 | |
| 781 | + | ||
| 782 | + | { 线程工作代码 ... } | |
| 783 | + | ||
| 784 | + | Result:=0; | |
| 785 | + | end; | |
| 689 | 786 | ``` | |
| 690 | 787 | ||
| 691 | - | ||
| 788 | + | ||
| 692 | 789 | ||
| 693 | - | ||
| 790 | + | ### 识别外部线程 | |
| 694 | 791 | ||
| 695 | - | ||
| 792 | + | 有时甚至不知道是否要处理外部线程(如某些 C 语言库带了[[callback|回调函数]])。以下步骤有助于开展分析: | |
| 696 | 793 | ||
| 697 | - | ||
| 794 | + | 1\. 应用启动后,向操作系统查询当前线程的ID。 | |
| 795 | + | ``` | |
| 698 | 796 | GetCurrentThreadID() // Windows; | |
| 699 | - | ||
| 700 | - | ||
| 797 | + | GetThreadID() // Darwin (macOS); FreeBSD; | |
| 798 | + | TThreadID(pthread_self) // Linux; | |
| 701 | 799 | ``` | |
| 702 | 800 | ||
| 703 | - | ||
| 801 | + | 2\. 在线程函数内部再次查询当前线程的 ID,与第1步的查询结果进行比较。 | |
| 704 | 802 | ||
| 705 | - | ||
| 803 | + | ### 放弃一些时间片 | |
| 706 | 804 | ||
| 707 | - | ||
| 805 | + | [ThreadSwitch](<https://www.freepascal.org/docs-html/rtl/system/threadswitch.html>) \- 注:可忽略。 | |
| 708 | 806 | ||
| 709 | - | ||
| 807 | + |  **Note:** 不要使用 Windows 特有的技巧 [_Sleep(0)_](<https://www.freepascal.org/docs-html/rtl/sysutils/sleep.html>),因为不适用于全部平台。 | |
| 710 | 808 | ||
| 711 | - | ||
| 809 | + | ## 参阅 | |
| 712 | 810 | ||
| 713 | - | ||
| 714 | - | ||
| 715 | - | ||
| 716 | - | ||
| 717 | - | ||
| 718 | - | ||
| 811 | + | * [[Streaming components|流组件]] | |
| 812 | + | * [[Manager Worker Threads System|工作线程系统的管理]] | |
| 813 | + | * [[Example of multi-threaded application: array of threads|多线程应用示例:线程数组]] | |
| 814 | + | * [[Parallel procedures|并行程序]] | |
| 815 | + | * [[Main Loop Hooks|主循环钩子]] | |
| 816 | + | * [[Asynchronous Calls|异步调用]] | |
| 817 | + | * [某个论坛贴子](<https://forum.lazarus.freepascal.org/index.php/topic,55164.0.html>) \- 有用的背景知识:为什么几乎绝不能在线程内访问 LCL 组件,以及可安全访问的部分。 | |
| 818 | + | * [多线程 - Delphi 的实现方式](<http://nickhodges.com/MultiThreadingInDelphi/ToC.html>) | |
| 819 | + | * [在 Delphi 应用中进行线程和 GUI 同步](<https://www.thoughtco.com/synchronizing-threads-and-gui-delphi-application-1058159>) | |
| 820 | + | * [介绍 Lazarus 中的线程编程](<https://www.freepascal.org/~michael/articles/lazthread/lazthread.pdf>) \- Michael Van Canneyt 的讲义 | |
| 719 | 821 | ||
| 720 | 822 | --- | |
| 721 | - | ||
| 823 | + | *From the Free Pascal wiki, page "Multithreaded Application Tutorial/zh CN", as the Internet Archive kept it on 2025-02-19, last changed on 2024-12-17. 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).* |
