Multithreaded Application Tutorial/zh CN
│
Deutsch (de) │
English (en) │
español (es) │
français (fr) │
日本語 (ja) │
polski (pl) │
português (pt) │
русский (ru) │
slovenčina (sk) │
中文(中国大陆) (zh_CN) │
概述
本文力图说清楚如何用 Free Pascal 和 Lazarus 编写和调试多线程应用。多线程应用会创建两个以上同时运行的线程。如果对多线程还不熟悉,请阅读“是否需要多线程”一节,以确定是否真的需要;这可能会避免很多麻烦。
多线程应用只有一个主线程,也即应用启动时由操作系统创建的那个线程。更新用户界面组件的只能是主线程,否则应用可能会被挂起。
多线程的主要思路是:应用可在第二个线程中后台处理一些任务,而用户可以继续使用主线程工作。
线程的另一个用途是让应用具备更好的响应能力。假设用户按下某个按钮后,应用开始处理一项大型任务……在处理过程中屏幕就会停止响应,用户会以为应用停滞不动了,用户体验就不大好,或引起误导。如果大型任务运行于另一个线程中,则应用几乎就能像空闲时一样保持响应。这时在启动大型任务的工作线程之前,最好是禁用窗体上的按钮,以免用户为同一任务启动多个线程。
多线程的另一个用途,可能就是服务器应用,以便能同时响应多个客户端的请求。
是否需要多线程?
如果对多线程编程不太熟悉,并且只是希望应用在执行稍长任务时能提升响应速度,那么可能并不一定需要用到多线程。多线程应用肯定更难调试,且往往更加复杂;很多情况下并不需要用到多线程。单线程就足以应付了。如果能将耗时任务拆分为多个部分,则应使用 Application.ProcessMessages 方法。此方法允许 LCL 处理所有等待的消息并返回。主要的思路是在执行长时间任务的过程中定期调用 Application.ProcessMessages,检测用户的点击,或者重绘进度条等。
例如:读取大文件并进行处理。参见示例文件 examples/multithreading/singlethreadingexample1.lpi。
多线程仅适用于:
- 阻塞句柄,比如网络通信
- 同时使用多个处理器 (SMP)
- 必须通过一次 API 调用完成的算法和库调用,无法分解成更小的部分
若要通过多线程同时使用多个处理器以提高速度,请先检查程序目前是否已经跑满了单核 CPU 的资源(例如,程序可能是在做大量输入输出操作,比如写入文件,这很耗时但并不占用大量 CPU 资源;这种情况下,即便用了多线程也不会更快)。另外,请检查优化级别是否已设为最高(3级)。将优化级别从1级升至3级,程序运行速度可能会提升5倍左右。
多线程应用所需单元
Windows 系统下实现多线程应用,不需要用到什么特别的单元。但是在 Linux、macOS 和 FreeBSD 系统中,需要用到 cthreads 单元,还必须是项目第一个引用的单元(程序源代码通常是 .lpr 文件)!有时 cthreads、cmem 和 cwstrings 等多个单元都要放在前面时,由于各单元之间的依赖关系,合理的顺序是cmem、cthreads、cwstrings。
所以 Lazarus 应用的源代码应如下所示:
program MyMultiThreadedProgram;
{$mode objfpc}{$H+}
uses
{$ifdef unix}
cthreads,
cmem, // 在某些系统中,多线程时 c 语言的内存管理速度会快很多
{$endif}
Interfaces, // 引入 LCL 可视化组件
Forms
{ 在此加入其它单元 },
如果忘记了 TThread 引用顺序,在应用启动时会显示如下错误:
This binary has no thread support compiled in. Recompile the application with a thread-driver in the program uses clause before other units using thread.
纯用 FPC 实现的代码示例
以下是非常简单的示例。在 Win7 及 FPC 3.0.4 中调试通过。
Program ThreadTest;
{test multi threading capability }
{
OUTPUT
thread 1 started
thread 1 thri 0 Len(S)= 1
thread 1 thri 1 Len(S)= 2
thread 1 thri 2 Len(S)= 3
thread 1 thri 3 Len(S)= 4
thread 1 thri 4 Len(S)= 5
thread 1 thri 5 Len(S)= 6
thread 1 thri 6 Len(S)= 7
thread 1 thri 7 Len(S)= 8
thread 1 thri 8 Len(S)= 9
thread 1 thri 9 Len(S)= 10
thread 1 thri 10 Len(S)= 11
thread 1 thri 11 Len(S)= 12
thread 1 thri 12 Len(S)= 13
thread 1 thri 13 Len(S)= 14
thread 1 thri 14 Len(S)= 15
thread 2 started
thread 3 started
thread 1 thri 15 Len(S)= 16
thread 2 thri 0 Len(S)= 1
thread 3 thri 0 Len(S)= 1
thread 1 thri 16 Len(S)= 17
...
...
thread 5 thri 997 Len(S)= 998
thread 5 thri 998 Len(S)= 999
thread 5 thri 999 Len(S)= 1000
thread 5 finished
thread 10 thri 828 Len(S)= 829
thread 9 thri 675 Len(S)= 676
thread 4 thri 656 Len(S)= 657
thread 10 thri 829 Len(S)= 830
thread 9 thri 676 Len(S)= 677
thread 9 thri 677 Len(S)= 678
thread 10 thri 830 Len(S)= 831
thread 10 thri 831 Len(S)= 832
thread 10 thri 832 Len(S)= 833
thread 10 thri 833 Len(S)= 834
thread 10 thri 834 Len(S)= 835
thread 10 thri 835 Len(S)= 836
thread 10 thri 836 Len(S)= 837
thread 10 thri 837 Len(S)= 838
thread 10 thri 838 Len(S)= 839
thread 10 thri 839 Len(S)= 840
thread 9 thri 678 Len(S)= 679
...
...
thread 4 thri 994 Len(S)= 995
thread 4 thri 995 Len(S)= 996
thread 4 thri 996 Len(S)= 997
thread 4 thri 997 Len(S)= 998
thread 4 thri 998 Len(S)= 999
thread 4 thri 999 Len(S)= 1000
thread 4 finished
10
}
uses
{$ifdef unix}cthreads, {$endif} sysutils;
const
threadcount = 10;
stringlen = 1000;
var
finished : longint;
threadvar
thri : ptrint;
function f(p : pointer) : ptrint;
var
s : ansistring;
begin
Writeln('thread ',longint(p),' started');
thri:=0;
while (thri<stringlen) do begin
s:=s+'1'; { create a delay }
writeln('thread ',longint(p),' thri ',thri,' Len(S)= ',length(s));
inc(thri);
end;
Writeln('thread ',longint(p),' finished');
InterLockedIncrement(finished);
f:=0;
end;
var
i : longint;
Begin
finished:=0;
for i:=1 to threadcount do
BeginThread(@f,pointer(i));
while finished<threadcount do ;
Writeln(finished);
End.
TThread 类
以下例子位于 examples/multithreading/ 目录中。
创建多线程应用最简单的方法就是用 TThread 类,能以简单的方式创建一个额外的线程(与主线程并行)。通常仅需重写两个方法:构造函数 Create 和 Execute 方法。
构造函数中需要进行线程运行的准备工作,设置变量或属性的初始值。TThread 的原始构造函数需一个 Suspended 参数。正如其名, Suspended 设为 True 会阻止线程创建后自动启动。如果 Suspended 设为 False,线程在创建后将立即开始运行。如果线程是以挂起状态创建的,则只会在调用 Start 方法后运行。
自 FPC 2.0.1 以后,TThread.Create 还有一个隐式的栈大小参数。可以按需修改所创建线程的默认栈大小。线程中的函数深度递归调用,就是需要扩大栈的很好例子。如未指定栈大小参数,则会采用操作系统的默认大小。
要在线程中执行的代码,通过重写 Execute 方法来完成。
TThread 类有一个重要属性:Terminated : boolean;
如果线程中包含循环(很常见),那么当 Terminated 为 true 时(默认为 false)应退出循环。每次迭代均须检查 Terminated 值,为 true 则应在完成必要的清理工作后尽快退出循环。请记住,Terminate 方法默认不执行任何操作:必须在 Execute 方法中明确实现线程退出工作的机制。Terminate 方法的工作只是将 Terminated 属性设为 True。
如前所述,线程不应与可视化组件发生交互。对可视化组件的更新必须在主线程中进行。
为实现与主线程的协作,TThread 类提供了 Synchronize 方法。Synchronize 需要一个不带参数的线程内部方法作为参数。以 Synchronize(@MyMethod) 的方式调用 MyMethod 时,线程将暂停运行,MyMethod 的代码将在主线程中调用,然后线程继续运行。
Synchronize 的准确实现方式依不同平台而定,但基本任务是:
- 发送一条消息到主消息队列,然后进入休眠状态;
- 主线程最终会处理这条消息,并调用 MyMethod。MyMethod 以没有上下文的方式调用,也就是不会在鼠标按下或界面绘制事件中,而是在这些事件之后调用;
- 主线程执行完 MyMethod 后,会唤醒休眠线程,并处理下一条消息;
- 线程继续运行。
TThread 还有一个重要的属性:FreeOnTerminate。如果 FreeOnTerminate 为 true,那么线程停止执行(Execute 方法)后,线程对象会自动释放。否则,应用需手动释放线程。
示例:
Type
TMyThread = class(TThread)
private
fStatusText : string;
procedure ShowStatus;
protected
procedure Execute; override;
public
Constructor Create(CreateSuspended : boolean);
end;
constructor TMyThread.Create(CreateSuspended : boolean);
begin
inherited Create(CreateSuspended);
FreeOnTerminate := True;
end;
procedure TMyThread.ShowStatus;
// 本方法由主线程执行,因此可以访问所有 GUI 元素。
begin
Form1.Caption := fStatusText;
end;
procedure TMyThread.Execute;
var
newStatus : string;
begin
fStatusText := 'TMyThread Starting...';
Synchronize(@Showstatus);
fStatusText := 'TMyThread Running...';
while (not Terminated) and ([其他条件]) do
begin
...
[here goes the code of the main thread loop]
...
if NewStatus <> fStatusText then
begin
fStatusText := newStatus;
Synchronize(@Showstatus);
end;
end;
end;
在应用程序中,
var
MyThread : TMyThread;
begin
MyThread := TMyThread.Create(True); // 这样线程就不会自动启动
...
[此处放置线程开始执行前的初始化代码]
...
MyThread.Start;
end;
若要应用更加灵活,可为线程创建一个事件;这样同步方法就不会与哪个窗体或类绑在一起:可在线程事件上绑定监听器。下面给出一个示例:
Type
TShowStatusEvent = procedure(Status: String) of Object;
TMyThread = class(TThread)
private
fStatusText : string;
FOnShowStatus: TShowStatusEvent;
procedure ShowStatus;
protected
procedure Execute; override;
public
Constructor Create(CreateSuspended : boolean);
property OnShowStatus: TShowStatusEvent read FOnShowStatus write FOnShowStatus;
end;
constructor TMyThread.Create(CreateSuspended : boolean);
begin
inherited Create(CreateSuspended);
FreeOnTerminate := True;
end;
procedure TMyThread.ShowStatus;
// 本方法由主线程执行,因此可以访问所有 GUI 元素。
begin
if Assigned(FOnShowStatus) then
begin
FOnShowStatus(fStatusText);
end;
end;
procedure TMyThread.Execute;
var
newStatus : string;
begin
fStatusText := 'TMyThread Starting...';
Synchronize(@Showstatus);
fStatusText := 'TMyThread Running...';
while (not Terminated) and ([any condition required]) do
begin
...
[here goes the code of the main thread loop]
...
if NewStatus <> fStatusText then
begin
fStatusText := newStatus;
Synchronize(@Showstatus);
end;
end;
end;
应用程序中的代码:
Type
TForm1 = class(TForm)
Button1: TButton;
Label1: TLabel;
procedure FormCreate(Sender: TObject);
procedure FormDestroy(Sender: TObject);
private
{ private declarations }
MyThread: TMyThread;
procedure ShowStatus(Status: string);
public
{ public declarations }
end;
procedure TForm1.FormCreate(Sender: TObject);
begin
inherited;
MyThread := TMyThread.Create(true);
MyThread.OnShowStatus := @ShowStatus;
end;
procedure TForm1.FormDestroy(Sender: TObject);
begin
MyThread.Terminate;
// FreeOnTerminate 为 true,因此不应再放置释放代码:
// MyThread.Free;
inherited;
end;
procedure TForm1.Button1Click(Sender: TObject);
begin
MyThread.Start;
end;
procedure TForm1.ShowStatus(Status: string);
begin
Label1.Caption := Status;
end;
特别注意事项
Windows 栈检查
在 Windows 中使用线程时,如果启用了 -Ct(栈检查)开关,可能会遇到一个令人头疼的问题。原因尚不明,如果用了默认堆栈大小,那么在每次调用 TThread.Create 时都会“触发”栈检查。目前唯一的变通办法就是不启用 -Ct 开关。请注意,异常不是在主线程中触发,而是在新建的线程中。于是会造成线程就像是未曾启动。
以下是一段良好的代码,用于检查这种情况以及线程创建过程中可能发生的其他异常:
MyThread := TThread.Create(False);
if Assigned(MyThread.FatalException) then
raise MyThread.FatalException;
上述代码确保线程创建过程中的所有异常都会在主线程中触发。
Windows 的 GetDC 函数
GetDC(及 GetDCEx)函数用于检索指定窗口区域或整个屏幕的设备上下文(DC)句柄。比如可用于屏幕截图。但请注意,GetDC 不是线程安全函数:根据文档说明,设备上下文句柄任何时候只能由一个线程使用。
多线程软件包
用到多线程的软件包,应在自定义选项中添加 -dUseCThreads 标志。打开包编辑器,然后选择“选项>用法(Usage)>自定义”,加入 -dUseCThreads。这样将为使用此软件包的所有项目和包(包括 IDE)定义此标志。IDE 及其创建的所有新应用已在 .lpr 文件中包含了以下代码:
uses
{$IFDEF UNIX}{$IFDEF UseCThreads}
cthreads,
cmem, // the c memory manager is on some systems much faster for multi-threading
{$ENDIF}{$ENDIF}
Heaptrc
在用到 cmem 单元时不能启用 -gh 开关。-gh 开关会用到 heaptrc 单元,而 heaptrc 单元对堆管理器的代码做了扩充。因此,在引用 cmem 单元时,heaptrc 单元必须加在后面。
uses
{$IFDEF UNIX}{$IFDEF UseCThreads}
cthreads,
cmem, // the c memory manager is on some systems much faster for multi-threading
{$ENDIF}{$ENDIF}
heaptrc,
线程的初始化和终止
若要对线程对象本身进行初始化,可以用挂起状态启动并设置属性,也可以新建一个构造函数并调用继承的构造函数。
注意:如 CreateSuspended=false,则使用 AfterConstruction 是很危险的,因为线程已经启动了。
析构函数则可用于释放对象所占资源。
type
TMyThread = class(TThread)
private
fRTLEvent: PRTLEvent;
public
constructor Create(SomeData: TSomeObject; CreateSuspended: boolean);
destructor Destroy; override;
end;
constructor TMyThread.Create(SomeData: TSomeObject; CreateSuspended: boolean);
begin
// 示例:设置事件、临界区、其他文件或数据库连接之类的资源
RTLEventCreate(fRTLEvent);
inherited Create(CreateSuspended);
end;
destructor TMyThread.Destroy;
begin
RTLeventDestroy(fRTLEvent);
inherited Destroy;
end;
非 LCL 程序
TThread.Synchronize 需要主线程定期调用 CheckSynchronize。LCL 会在事件循环中进行调用。如果不使用 LCL 的事件循环,那就必须自行完成调用。
SMP支持
好消息是,如果你的应用程序在多线程上工作正常,看来SMP已经启用。
Lazarus 调试多线程应用
Lazarus 的调试需要 GDB,并且其功能正在迅速变得更加全面和稳定。但仍有一些 Linux 版本存在问题。
调试时的输出
在单线程应用中,写入控制台/终端/其他输出设备都很简单,并且输出顺序与写入顺序相同。但多线程应用中的情况要复杂得多。如果两个线程同时写入,比如线程A先写一行,线程B再写一行,这两行不一定会按这个顺序写入。甚至可能一个线程在输出时,另一个线程也在写入。在 Linux 下,DebugLn() 输出(可能)会得到正确顺序 。但在 Win32 下,由于是在主线程之外使用 DebugLn(),可能会引发异常(可能是 DiskFull)。因此,为了避免麻烦,请使用以下 DebugLnThreadLog()。
LCLProc 单元中有几个函数,可供每个线程写入自己的日志文件:
procedure DbgOutThreadLog(const Msg: string); overload;
procedure DebuglnThreadLog(const Msg: string); overload;
procedure DebuglnThreadLog(Args: array of const); overload;
procedure DebuglnThreadLog; overload;
比如,不用 writeln('Some text ',123); 而是用
DebuglnThreadLog(['Some text ',123]);
这样就会在 Log<PID>.txt 文件中追加一行 'Some text 123',其中 <PID> 是当前线程的进程 ID。
在每次运行线程前删除日志文件,是个好办法:
rm -f Log* && ./project1
Linux
若要在 Linux 中调试多线程应用,会碰到一个大问题:X Windows 的桌面管理器可能会挂起。比如应用捕获了鼠标/键盘并被 gdb 暂停时就会发生这种情况,此时 X 系统会等待应用响应。这时只要从另一台计算机登录并杀掉 gdb 即可,或者按 CTRL+ALT+F3 退出会话并杀掉 gdb。另外也可以重启窗口管理器:输入 sudo /etc/init.d/gdm restart,会重新回到桌面。
gdb 停止应用程序的时机不一定每次都一样,因此有些技巧还是有用的:Ubuntu x64 系统下可设置一下项目的选项,以便获得调试所需的额外信息文件……
项目选项->编译器选项->调试,勾选“使用外部调试符号文件(-Xg)”。
另一种做法是再开一个X桌面,一个桌面运行 IDE/gdb,另一个桌面运行应用程序,这样只会挂起调试的桌面。用以下命令新建 X 实例:
X :1 &
这样就会新开一个桌面,切换到另一个桌面(比如按 CTRL+ALT+F7)时,就能按 CTRL+ALT+F8 返回新开的桌面(如果组合键不起作用,尝试按CTRL+ALT+F2 …… 在 Slackware 中是有效的)。
然后即可创建桌面会话如下:
gnome-session --display=:1 &
然后,在 Lazarus 中,在项目的“运行参数”对话框中,勾选“使用显示”并输入 :1。
应用将运行于第二个 X 服务上,且可在第一个 X 服务上进行调试。
已在 Windows 和 Linux 中 Free Pascal 2.0 和 Lazarus 0.9.10 通过测试。
不新开 X 会话的话,还可以使用 Xnest。Xnest 是运行于窗口上的 X 会话。调试线程时使用了 Xnest,X 服务就不会锁住,而且不用来回切换终端,调试起来更加容易。
运行 Xnest 的命令行:
Xnest :1 -ac
以上将在:1创建一个 X 会话,并禁用访问控制。
Lazarus 界面组件的接口
Win32、gtk 和 carbon 接口都支持多线程,就是说 TThread、临界区和 Synchronize 都能用。但这些接口并不是线程安全的,就是说一次只能有一个线程访问 LCL。并且由于主线程永远不允许等待其他线程,就是说只有主线程才能访问 LCL,包括所有与 TControl、Application 和 LCL 控件句柄相关的操作。LCL 中有一些函数是线程安全的,比如 FileUtil 单元中的大多数函数。
利用 SendMessage/PostMessage 进行线程间通信
一个应用应该只有一个线程(通常是主线程)会去调用 LCL API。其他线程可以通过多种间接方法使用 LCL,其中一种不错的做法就是利用 SendMessage 或 PostMessage。LCLIntf.SendMessage 和 LCLIntf.PostMessage 会一条带有窗口指向的消息送入应用的消息池。
关于这两个函数,请参看以下文档:
SendMessage 和 PostMessage 的区别在于,将控制权交回调用线程的方式。与 Synchronize 类似,SendMessage 会阻塞调用线程,直到目标窗口完成消息处理后才返回控制权;但在某些情况下,SendMessage 可能需要待在调用线程的上下文中进行优化操作。而 PostMessage 则会立即返回控制权,只要排队的消息数量不超过系统定义的最大值,并且堆上仍有填加数据的空间。
无论采用哪种发送方式,在消息处理函数中(见下文)都应避免调用 application.ProcessMessages,因为可能会导致再发送一次消息,消息将递归处理。如果无法避免,那么最好采用其他机制在线程之间传递串行事件。
以下是一个示例,展示了辅助线程如何将文本发给主线程,显示于 LCL 控件中:
const
WM_GOT_ERROR = LM_USER + 2004;
WM_VERBOSE = LM_USER + 2005;
procedure VerboseLog(Msg: string);
var
PError: PChar;
begin
if MessageHandler = 0 then Exit;
PError := StrAlloc(Length(Msg)+1);
StrCopy(PError, PChar(Msg));
PostMessage(formConsole.Handle, WM_VERBOSE, Integer(PError), 0);
end;
下面是处理窗口消息的示例:
const
WM_GOT_ERROR = LM_USER + 2004;
WM_VERBOSE = LM_USER + 2005;
type
{ TformConsole }
TformConsole = class(TForm)
DebugList: TListView;
// ...
private
procedure HandleDebug(var Msg: TLMessage); message WM_VERBOSE;
end;
var
formConsole: TformConsole;
implementation
....
{ TformConsole }
procedure TformConsole.HandleDebug(var Msg: TLMessage);
var
Item: TListItem;
MsgStr: PChar;
MsgPasStr: string;
begin
MsgStr := PChar(Msg.wparam);
MsgPasStr := StrPas(MsgStr);
Item := DebugList.Items.Add;
Item.Caption := TimeToStr(SysUtils.Now);
Item.SubItems.Add(MsgPasStr);
Item.MakeVisible(False);
// 下面可干点事情,比如:
TrayControl.SetError(MsgPasStr);
StrDispose(MsgStr)
end;
end.
如果在 Linux x64 机器上遇到错误:“Project XY raised exception class 'External: SIGSEGV'”时,就得将“VerboseLog”中的“Integer(PError)”修改为“PtrInt(PError)”。修改后的这行代码应如下所示:
PostMessage(formConsole.Handle, WM_VERBOSE, PtrInt(PError), 0);
临界区(critical section)
临界区是一个对象,用于确保某部分代码在任何时候只能由一个线程执行。临界区在使用前需要创建/初始化,在不用时需释放。
临界区有两种使用方式,RTL 或 LCL。以下给出 RTL 的用法,平替的 LCL 方式则以注释的形式给出,唯一区别就是方法名不同。
使用临界区时通常如下引入:
// uses LCLIntf, LCLType;
所需的 RTL 单元几乎都可涵盖。
声明(对所有要访问临界区的线程而言,是全局变量):
MyCriticalSection: TRTLCriticalSection;
// MyCriticalSection : TCriticalSection;
创建:
InitCriticalSection(MyCriticalSection);
//InitializeCriticalSection(MyCriticalSection);
运行一些线程。独占地执行某些操作:
EnterCriticalSection(MyCriticalSection);
try
// 访问变量、写入文件、发送网络包等
finally
LeaveCriticalSection(MyCriticalSection);
end;
// RTL 和 LCL 的语法相同,不错吧。
所有线程结束后,释放临界区:
DoneCriticalSection(MyCriticalSection);
// DeleteCriticalSection(MyCriticalSection);
另一种方案是使用 TCriticalSection 对象。对象创建即初始化,Enter 方法执行 EnterCriticalSection 操作,Leave 方法执行 LeaveCriticalSection 操作,而对象销毁则执行删除操作。
请注意,临界区无法防止同一线程多次进入同一块代码,而只能防止不同线程同时进入。因此,临界区不能用于防止消息处理函数的重入(请参阅前文)。
示例:5个线程同时递增1个计数器。 请参阅 lazarus/examples/multithreading/criticalsectionexample1.lpi。
Warning: 上述4个函数分成了两组,分别由 RTL 和 LCL 提供。LCL 的函数在 LCLIntf 和 LCLType 单元中定义。这两组函数的工作方式大致相同,应用程序中可以同时使用,但不得在 LCL 临界区内使用 RTL 函数,反之亦然。
共享变量
如果有多个线程共享一个只读变量,那没什么可担心的。直接读就是了。 但如果有一个或多个线程要修改变量值,那就必须确保每次只有一个线程能够访问。
示例:5个线程递增1个计数器。 参见 lazarus/examples/multithreading/criticalsectionexample1.lpi
等待其他线程
如果线程A需要用到线程B的运行结果,那就必须等待至线程B完成。
重要:主线程绝不能等待其他线程,而应使用 Synchronize 方法(参见前文)。
示例参见:lazarus/examples/multithreading/waitforexample1.lpi
{ TThreadA }
procedure TThreadA.Execute;
begin
Form1.ThreadB:=TThreadB.Create(false);
// 创建事件
WaitForB:=RTLEventCreate;
while not Application.Terminated do begin
// 一直等待,直至线程B唤醒A
RtlEventWaitFor(WaitForB);
writeln('A: ThreadB.Counter='+IntToStr(Form1.ThreadB.Counter));
end;
end;
{ TThreadB }
procedure TThreadB.Execute;
var
i: Integer;
begin
Counter:=0;
while not Application.Terminated do begin
// 线程 B 执行任务
Sleep(1500);
inc(Counter);
// 唤醒线程 A
RtlEventSetEvent(Form1.ThreadA.WaitForB);
end;
end;
Fork
多线程应用在创建子进程时,请注意在 fork(或 fpFork)调用之前创建并运行的所有线程均不会在子进程中运行。正如 fork() 手册所述,任何在 fork 调用之前运行的线程,状态将是未定义。
因此,请注意在 fork 调用之前初始化的任何线程(包括 initialization 部分中的线程)都不会工作。
并行过程/循环
多线程的一个特例是并行运行单个过程。请参见并行过程。
分布式计算
多线程的更高境界是在多台机器上运行这些线程。
- 可以使用 synapse、lnet、indy 之类的 TCP 组件进行通信,以提供最大的灵活性,通常用于松散连接的客户端/服务器应用。
- 可以使用 MPICH 这种消息传递库,用于在服务器集群上实现 HPC(高性能计算)。
外部线程
为了让 Free Pascal 的线程系统能够正常运转,新建 FPC 线程都需要进行初始化(更准确地说,每个线程的异常、I/O系统和 threadvar 系统都需要初始化,以确保本地变量和内存堆能正常工作)。如果使用了 BeginThread(或通过 TThread 类间接使用了),那么这些初始化工作都会自动完成。但如果用到的线程不是由 BeginThread 创建的(即外部线程),则(目前)可能需要多做一些初始化的工作。外部线程还包括在外部 C 语言库(.DLL/.so)中创建的线程。
以下是使用外部线程时需要考虑的事项(可能并非所有版本编译器都需要考虑):
- 完全不使用外部线程,而应该采用 FPC 线程。如果能控制线程的创建方式,请通过 BeginThread 自行创建线程。
如果调用约定不合适(比如原线程函数遵守 cdecl 调用方式,而 BeginThread 是 pascal 调用方式),则可以创建一个记录,存入需调用的原线程函数,并在 pascal 线程函数中调用,如下所示:
type
TCdeclThreadFunc = function (user_data:Pointer):Pointer;cdecl;
PCdeclThreadFuncData = ^TCdeclThreadFuncData;
TCdeclThreadFuncData = record
Func: TCdeclThreadFunc; //cdecl 函数
Data: Pointer; //原数据
end;
// Pascal 线程调用 cdecl 函数
function C2P_Translator(FuncData: pointer) : ptrint;
var
ThreadData: TCdeclThreadFuncData;
begin
ThreadData := PCdeclThreadFuncData(FuncData)^;
Result := ptrint(ThreadData.Func(ThreadData.Data));
end;
procedure CreatePascalThread;
var
ThreadData: PCdeclThreadFuncData;
begin
New(ThreadData);
// 下面是需调用的 cdecl 线程函数
ThreadData^.Func := func;
ThreadData^.Data := user_data;
// 创建 Pascal 线程
BeginThread(@C2P_Translator, ThreadData );
end;
- 通过创建一个虚拟线程来初始化 FPC 的线程系统。如果应用中未创建任何 Pascal 线程,那么线程系统将不会初始化(因此,本地变量和内存堆都不会正常工作)。
type
tc = class(tthread)
procedure execute;override;
end;
procedure tc.execute;
begin
end;
{ main program }
begin
{ initialise threading system }
with tc.create(false) do
begin
waitfor;
free;
end;
{ ... 下面是自己的代码 }
end.
(线程系统初始化后,运行时可能会将系统变量“IsMultiThread”置为 true,FPC 函数利用此变量执行各种加解锁操作。此变量不得自行改动。)
- 如果上述方法无效,请在外部线程函数中尝试加入以下代码:
function ExternalThread(param: Pointer): LongInt; stdcall;
var
tm: TThreadManager;
begin
GetThreadManager(tm);
tm.AllocateThreadVars;
InitThread(1000000); // 在此调整初始栈的大小
{ 线程工作代码 ... }
Result:=0;
end;
识别外部线程
有时甚至不知道是否要处理外部线程(如某些 C 语言库带了回调函数)。以下步骤有助于开展分析:
1. 应用启动后,向操作系统查询当前线程的ID。
GetCurrentThreadID() // Windows;
GetThreadID() // Darwin (macOS); FreeBSD;
TThreadID(pthread_self) // Linux;
2. 在线程函数内部再次查询当前线程的 ID,与第1步的查询结果进行比较。
放弃一些时间片
ThreadSwitch - 注:可忽略。
参阅
- 流组件
- 工作线程系统的管理
- 多线程应用示例:线程数组
- 并行程序
- 主循环钩子
- 异步调用
- 某个论坛贴子 - 有用的背景知识:为什么几乎绝不能在线程内访问 LCL 组件,以及可安全访问的部分。
- 多线程 - Delphi 的实现方式
- 在 Delphi 应用中进行线程和 GUI 同步
- 介绍 Lazarus 中的线程编程 - Michael Van Canneyt 的讲义