Embarcadero Delphi 使用线程进行后台工作的响应式GUI,使用PostMessage从线程进行报告

示例

要在长时间运行过程中使GUI保持响应状态,需要进行一些非常精心的“回调”以允许GUI处理其消息队列,或者使用(后台)(工作线程)线程。

通常,启动任意数量的线程来执行某些工作不是问题。当您想让GUI显示中间和最终结果或报告进度时,乐趣就开始了。

在GUI中显示任何内容都需要与控件和/或消息队列/泵进行交互。那应该总是在主线程的上下文中完成。永远不要在任何其他线程的上下文中。

有很多方法可以解决这个问题。

这个例子显示了如何使用简单的线程,允许它通过设置完成线程实例后的GUI访问做FreeOnTerminate来false使用,当一个线程“完成”的报告PostMessage。

关于竞争条件的注释:对辅助线程的引用以表格的形式保存在数组中。线程完成后,数组中的相应引用将变为nil-ed。

这是比赛条件的潜在来源。与使用“运行”布尔值一样,可以更轻松地确定是否还有任何线程需要完成。

您将需要确定是否需要使用锁来保护这些资源。

就本例而言,没有必要。它们仅在两个位置进行修改:StartThreads方法和HandleThreadResults方法。这两种方法仅在主线程的上下文中运行。只要您保持这种方式并且不从不同线程的上下文开始调用这些方法,它们就不可能产生竞争条件。

线

type
  TWorker = class(TThread)
  private
    FFactor: Double;
    FResult: Double;
    FReportTo: THandle;
  protected
    procedure Execute; override;
  public
    constructor Create(const aFactor: Double; const aReportTo: THandle);

    property Factor: Double read FFactor;
    property Result: Double read FResult;
  end;

构造函数只设置私有成员并将FreeOnTerminate设置为False。这是必不可少的,因为它将允许主线程向线程实例查询其结果。

execute方法进行计算,然后将一条消息发布到在其构造函数中接收到的句柄,以表示已完成:

procedure TWorker.Execute;
const
  Max = 100000000;var
  i : Integer;
begin
  inherited;

  FResult := FFactor;
  for i := 1 to Max do
    FResult := Sqrt(FResult);

  PostMessage(FReportTo, UM_WORKERDONE, Self.Handle, 0);
end;

PostMessage在此示例中,必不可少的使用。PostMessage“ just”将消息放入主线程的消息泵的队列中,而不等待它被处理。它本质上是异步的。如果要使用,SendMessage您将自己编码为泡菜。SendMessage将消息放入队列,并等待直到消息被处理。简而言之,它是同步的。

自定义UM_WORKERDONE消息的声明声明为:

const
  UM_WORKERDONE = WM_APP + 1;
type
  TUMWorkerDone = packed record
    Msg: Cardinal;
    ThreadHandle: Integer;
    unused: Integer;
    Result: LRESULT;
  end;

的UM_WORKERDONE常量的用途WM_APP,作为其值的起点,以确保它不与由Windows或Delphi的VCL(所推荐的微软)使用的任何值干扰。

形成

任何形式都可以用来启动线程。您需要做的就是向其中添加以下成员:

private
  FRunning: Boolean;
  FThreads: array of record
    Instance: TThread;
    Handle: THandle;
  end;
  procedure StartThreads(const aNumber: Integer);
  procedure HandleThreadResult(var Message: TUMWorkerDone); message UM_WORKERDONE;

哦,示例代码假定Memo1: TMemo;表单的声明中存在a ,它用于“记录和报告”。

该FRunning可用于防止GUI从开始时的工作是怎么回事被点击。FThreads用于保存实例指针和创建的线程的句柄。

启动线程的过程非常简单。首先检查是否已经有一组线程正在等待。如果是这样,它将退出。如果不是,则将标志设置为true,并启动为线程提供各自的句柄的线程,以便它们知道将其“完成”消息发布到何处。

procedure TForm1.StartThreads(const aNumber: Integer);
var
  i: Integer;
begin
  if FRunning then
    Exit;
    
  FRunning := True;

  Memo1.Lines.Add(Format('Starting %d worker threads', [aNumber]));
  SetLength(FThreads, aNumber);
  for i := 0 to aNumber - 1 do
  begin
    FThreads[i].Instance := TWorker.Create(pi * (i+1), Self.Handle);
    FThreads[i].Handle := FThreads[i].Instance.Handle;
  end;
end;

线程的句柄也放置在数组中,因为这是我们在告诉我们线程已完成的消息中收到的信息,并将其置于线程实例的外部使得访问起来稍微容易一些。如果不需要实例来获取结果(例如,如果它们已存储在数据库中)FreeOnTerminate,True则在线程实例外部使用句柄还允许我们使用set 。在这种情况下,当然无需保留对该实例的引用。

有趣的是在HandleThreadResult实现中:

procedure TForm1.HandleThreadResult(var Message: TUMWorkerDone);
var
  i: Integer;
  ThreadIdx: Integer;
  Thread: TWorker;
  Done: Boolean;
begin
  // 在数组中查找线程
  ThreadIdx := -1;
  for i := Low(FThreads) to High(FThreads) do
    if FThreads[i].Handle = Cardinal(Message.ThreadHandle) then
    begin
      ThreadIdx := i;
      Break;
    end;

  // 报告结果并释放线程,调零其指针和句柄 
  // 这样我们就可以检测到所有线程何时完成。
  if ThreadIdx > -1 then
  begin
    Thread := TWorker(FThreads[i].Instance);
    Memo1.Lines.Add(Format('Thread %d returned %f', [ThreadIdx, Thread.Result]));
    FreeAndNil(FThreads[i].Instance);
    FThreads[i].Handle := nil;
  end;

  // 查看是否所有线程都已完成。
  Done := True;
  for i := Low(FThreads) to High(FThreads) do
    if Assigned(FThreads[i].Instance) then
    begin
      Done := False;
      Break;
    end;
  if Done then
  begin
    Memo1.Lines.Add('Work done');
    FRunning := False;
  end;
end;

此方法首先使用消息中接收到的句柄查找线程。如果找到匹配项,它将使用实例(FreeOnTerminatewas False,还记得吗?)检索并报告线程的结果,然后完成操作:释放实例并将实例引用和句柄都设置为nil,表明该线程不再相关。

最后,它检查是否有任何线程仍在运行。如果未找到,则报告“所有完成”,并将FRunning标志设置为,False以便可以开始新的一批工作。