基于 QWorker 的多线程编程 – 后台线程与用户界面交互

我们知道,后台作业不能直接访问和操作用户界面元素。实际上,所谓的直接访问并不是绝对的,但属性这个东西你读时,可能执行的是一个读函数,它是否内部进行了一些不安全的写操作,有时候是叫不准的,那么,显然不在后台作业中访问更安全。

那么,我们如果需要和前面的用户界面交互该怎么做?

在进一步讨论之前,我们首先要明白和主线程进行交互,实际上存在同步和异步两种情况的:

(1)、异步的交互适合通知类型的情况,它只是将要交互的内容发往主线程,由主线程根据相应的参数进行处理。

(2)、同步的交互适合于需要等待主线程处理结果的情况,它需要将交互的内容发往主线程后,由主线程返回结果给自己。

那么,同步的异步真的是绝对的吗?当然不是,实际上同步的操作可以很容易的转换为异步的操作。比如,我们异步通知主线程后,然后由主线程执行操作完后,再触发一个后台作业的异步执行,那效果和同步实际上并没有什么本质的区别。只是同步的编程模型在许多时候,更容易理解,代码的可读性更好。但反过来,同步的编程模型必需等待结果返回,在此过程中,当前线程必需等待而不能处理其它任务,从而影响处理效率。所以,我们一般推荐都是异步来进行处理的。

在 QWorker 中,我们推荐的基本处理流程是:

(1)、后台作业投寄主线程作业后退出;

(2)、主线程作业完成后,触发新的后台作业后退出;

(3)、新的后台作业进行进一步处理;

在这些步骤中,参数的传递可以通过AData成员传递,也可以通过将作业过程写成一个类的成员,然后直接访问类的成员变量来解决(类似于下面的分组作业处理)。

但是,作为一个有良心的开发者,我还是希望你了解的更多,所以我们进一步来阐述这一问题。

上面的步骤如果多的话,你可能觉得自己处理有点麻烦,所以 QWorker 为你提供了作业分组对象:TQJobGroup。你只需要创建一个顺序执行的作业分组,然后将步骤依次加入到分组,然后调用 TQJobGroup.Run 就好了,TQJobGroup 会依次执行作业,而不需要你再手工投寄。当然了,作业的公共参数也就不能通过作业的 AData 成员传递了,而应该将其赋值给 TQJobGroup 的 Tag 标签,以便所有的作业共享,或者你创建一个类,将所有的公共参数和作业处理函数都做为这个类的成员,这样子就两全齐美了,不用受标签的 Tag 指针的限制了。大概示意如下:

【Delphi】

TMyGroupJobs=class
protected
   //自己的变量声明,然后声明作业分组对象
   FGroup:TQJobGroup;
   //声明作业处理步骤函数
   procedure DoStep_1(AJob:PQJob);
   ...
   procedure DoStep_n(AJob:PQJob);
   //用于响应FGroup.AfterDone事件,在所有作业完成时自动释放
   proceduer DoJobsDone(ASender:TObject);
public
   constructor Create;overload;
   destructor Destroy;override;   
end;
...
constructor TMyGroupJobs.Create;
begin
inherited;
...
FGroup:=TQJobGroup.Create(True);
FGroup.AfterDone:=DoJobsDone;
FGroup.Prepare;
FGroup.Add(DoStep_1..
FGroup.Run;
...
end;

constructor TMyGroupJobs.Destory;
begin
FGroup.Cancel;
FGroup.Free;
inherited;
end;

proceduer TMyGroupJobs.DoJobsDone(ASender:TObject);
begin
FreeObject(Self);
end;
...
AJobs:=TMyGroupJobs.Create;
....

【C++ Builder】

class TMyGroupJobs
{
protected:
   //自己的变量声明,然后声明作业分组对象
   TQJobGroup *FGroup;
   //声明作业处理步骤函数
   void __fastcall DoStep_1(PQJob AJob);
   ...
   void __fastcall DoStep_n(PQJob AJob);
   //用于响应FGroup.AfterDone事件,在所有作业完成时自动释放
   void __fastcall DoJobsDone(TObject *ASender)
   {
   delete self;
   }
public:
   __fastcall TMyGroupJobs()
   {
   ...
   FGroup=new TQJobGroup(true);
   FGroup->AfterDone=DoJobsDone;
   FGroup->Prepare();
   FGroup->Add(DoStep_1,...
   FGroup->Run();
   }
   __fastcall ~TMyGroupJobs()
   {
   ...
   FGroup->Cancel();
   delete FGroup;
   }
};
...
AJobs=new TMyGroupJobs;
....

只需要将在主线程中运行的作业投递到主线程去执行就好了,剩下的就是等待它完成了。什么,内存泄露?AJobs在你保存下,退出前自己释放下不就好了,这个不需要我告诉你吧。

好了,虽然说的是 QWorker,但我不介意提一下原生的方式:

(1)、PostMessage 发送通知消息到主线程,然后在主线程中响应消息处理。

(2)、SendMessage 发送通知到主线程,然后等待主线程中完成处理并返回结果。

无论 PostMessage 还是 SendMessage 函数,它们都是线程安全的,可以放心的投递而不用担心。唯一需要担心的是 PostMessage 的消息在特定的情况下,可能会得不到执行的机会(窗口在处理该消息之前被重建或销毁,原来的句柄就无效了)。当然,本着一切自愿,风险自担的原则,请大家放心使用。对了,提示下 FMX 平台下没有 PostMessage,但我们为您提供了一个模拟函数,参考主题:在FMX中实现PostMessage的方法,这里就不啰嗦了。

【注】QWorker 实际上信号作业也可以在主线程和后台线程之间调度作业完成交互,参数的共享也只需要象前述一样解决就好。

分享到: