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;此方法首先使用消息中接收到的句柄查找线程。如果找到匹配项,它将使用实例(FreeOnTerminatewasFalse,还记得吗?)检索并报告线程的结果,然后完成操作:释放实例并将实例引用和句柄都设置为nil,表明该线程不再相关。
最后,它检查是否有任何线程仍在运行。如果未找到,则报告“所有完成”,并将FRunning标志设置为,False以便可以开始新的一批工作。