要在长时间运行过程中使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以便可以开始新的一批工作。