Multithreaded Application Tutorial/zh CN

From Free Pascal wiki
Jump to navigationJump to search

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.
Light bulb  Note: 如果链接时报错找不到“mcount”,可能是因为引用了包含多线程代码的某个单元,此时需添加 cthreads 单元或启用智能链接功能。
Light bulb  Note: 如果报错“Project raised exception class 'RunError(232)' in procedure SYSTEM_NOTHREADERROR”,说明代码用到了线程,需添加 cthreads 单元的引用。


纯用 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 方法后运行。

Light bulb  Note: Resume 方法自FPC 2.4.4 后已弃用,已由 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-icon.png

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;
Light bulb  Note: RtlEventSetEvent可在 RtlEventWaitFor 之前调用。RtlEventWaitFor 将立即返回。可用 RTLeventResetEvent 清除标志。

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 - 注:可忽略。

Light bulb  Note: 不要使用 Windows 特有的技巧 Sleep(0),因为不适用于全部平台。

参阅