示例-QWorker后台线程与前台通讯演示解析

关于前后台通讯这一点,我在文章 QWorker技巧之作业与主线程之间通讯 中对此进行了一些说明,但一直没有给出单独的示例。因此在群里,许多朋友对QWorker如何和前台线程通讯还是有点犯迷糊。所以,我特意写了一个小演示程序,来演示如何做到这一点。

这个示例在主线程中是要分别更新10个进度条的进度信息,先看一下截图:

ProgressDemo_1

声明下,本示例演示了如何在QWorker中后台作业与界面元素进行交互的基本方法,当然这不是唯一的方法,主要是提供一个参考,更多的方法参考前面提到的文章。
本示例演示了一个三步作业:

  1. 分别填充8个TStringList对象,并显示填充进度。
  2. 合并8个TStringList的结点到第一个TStringList
  3. 在后台线程中遍历合并后的TStringList,来尝试指到特定的字符 A 出现的次数。

本示例用到了以下功能:

  1. 使用TQJobGroup做了一个串行化作业,保证上面的三步按顺序执行。
  2. 使用TQWorkers.For做了并行计算,填充8个TStringList列表。
  3. 使用TQMsgPack来传递进度参数到主线程来更新进度。

好了,现在我们来解析代码:

procedure TForm6.Button1Click(Sender: TObject);
var
  AGroup: TQJobGroup;
  ALists: TStringListArray;
  I: Integer;
begin
Gauge1.Progress := 0;
Gauge2.Progress := 0;
Gauge3.Progress := 0;
Gauge4.Progress := 0;
Gauge5.Progress := 0;
Gauge6.Progress := 0;
Gauge7.Progress := 0;
Gauge8.Progress := 0;
Gauge9.Progress := 0;
Button1.Enabled := False;
SetLength(ALists, 8);
AGroup := TQJobGroup.Create(True);
AGroup.Prepare;
for I := 0 to 7 do
  ALists[I] := TStringList.Create;
AGroup.Add(DoCreateListData, @ALists, False);
AGroup.Add(DoMergeListData, @ALists, False);
AGroup.Add(DoSearchChar, ALists[0], False);
AGroup.Run();
AGroup.MsgWaitFor();
Gauge10.Progress := 100;
FreeAndNil(AGroup);
for I := 0 to 7 do
  FreeAndNil(ALists[I]);
ShowMessage('字符 A 重复次数为:' + IntToStr(FRepeatTimes));
Button1.Enabled := True;
end;

这一段代码前面初始化进度条位置为0并设置按钮的Enabled属性为False,以避免重复点击。然后先创建了一个TQJobGroup对象和8个TStringList对象,然后依次添加了三项要顺序执行的作业,然后调用Run和MsgWaitFor来等待作业执行完成。注意这里由于我们要与主线程交互,所以一定要用MsgWaitFor而不是WaitFor,以避免阻塞主线程的消息处理。作业执行完成后,当然就是清理的过程和显示查找的结果,恢复状态,咱就略过不表了。

procedure TForm6.DoUpdateProgress(AJob: PQJob);
var
  AMsgPack: TQMsgPack;
begin
AMsgPack := AJob.Data;
case AMsgPack.IntByName('Index', -1) of
  0:
    Gauge1.Progress := AMsgPack.IntByName('Progress', 0);
  1:
    Gauge2.Progress := AMsgPack.IntByName('Progress', 0);
  2:
    Gauge3.Progress := AMsgPack.IntByName('Progress', 0);
  3:
    Gauge4.Progress := AMsgPack.IntByName('Progress', 0);
  4:
    Gauge5.Progress := AMsgPack.IntByName('Progress', 0);
  5:
    Gauge6.Progress := AMsgPack.IntByName('Progress', 0);
  6:
    Gauge7.Progress := AMsgPack.IntByName('Progress', 0);
  7:
    Gauge8.Progress := AMsgPack.IntByName('Progress', 0);
  8:
    Gauge9.Progress := AMsgPack.IntByName('Progress', 0);
  9:
    Gauge10.Progress := AMsgPack.IntByName('Progress', 0);
end;

end;

procedure TForm6.NotifyProgress(AIndex, AProgress: Integer);
var
  AParams: TQMsgPack;
begin
AParams := TQMsgPack.Create;
AParams.Add('Index', AIndex);
AParams.Add('Progress', AProgress); // (I+1)*100/10000=>(I+1)/100
Workers.Post(DoUpdateProgress, AParams, True, jdfFreeAsObject);
end;

这两个函数用于DoUpdateProgress根据传过来的参数,来更新10个进度条的进度。而NotifyProgress函数则用于触发DoUpdateProgress在主线程中执行。由于需要参数,因此,使用了TQMsgPack来组合这两个参数,当然,你也可以用一个结构,然后用TQJobExtData来管理它。前面也说了,这里只是多个方案之一。

继续看下一个代码:

procedure TForm6.DoCreateListData(AJob: PQJob);
begin
Workers.&For(0, 7, DoFillListData, False, AJob.Data);
end;

procedure TForm6.DoFillListData(ALoopMgr: TQForJobs; AJob: PQJob;
  AIndex: NativeInt);
var
  AList: TStringList;
  I: Integer;
  T: Cardinal;
  function RandomString: String;
  var
    C: Integer;
    p: PChar;
  begin
  C := 1 + random(100);
  SetLength(Result, C);
  p := PChar(Result);
  while C > 0 do
    begin
    p^ := WideChar(Ord('A') + random(32));
    Inc(p);
    Dec(C);
    end;
  end;

begin
AList := PStringListArray(AJob.Data)^[AIndex];
AList.Capacity := PerListCount; 
T := GetTickCount;
for I := 0 to PerListCount - 1 do
  begin
  AList.Add(RandomString);
  if GetTickCount - T > 50 then // 每50ms更新进度一次
    begin
    T := GetTickCount;
    NotifyProgress(AIndex, (I + 1) div 100);
    end;
  end;
NotifyProgress(AIndex, 100);
end;

第一个作业处理函数DoCreateListData使用For并行来调用DoFillListData来并行填充这8个列表。然后添加过程中,每50ms通知一下主线程更新进度信息。

procedure TForm6.DoMergeListData(AJob: PQJob);
var
  I: Integer;
  ALists: PStringListArray;
begin
ALists := AJob.Data;
ALists^[0].Capacity := MergedCount;
for I := 1 to 7 do
  begin
  ALists^[0].AddStrings(ALists^[I]);
  NotifyProgress(8, I * 100 div 7);
  end;
// 因为这个顺序作业执行时,没有别的线程写这个变量,所以初始化它是安全的
FRepeatTimes := 0;
end;

合并列表作业DoMergeListData合并列表内容,并每合并完一个调用NotifyMessage通知一次进度。直接到合完成。这里的FRepeatTimes实际上在Button1Click里初始化更合适,放在这里我只是想提醒大家,多线程编程中,如果你能确定一个变量没有同时读写,那么直接访问它就是安全的。

procedure TForm6.DoSearchChar(AJob: PQJob);
begin
Workers.&For(0, MergedCount - 1, DoSearchChar, False, AJob.Data);
end;

procedure TForm6.DoSearchChar(ALoopMgr: TQForJobs; AJob: PQJob;
  AIndex: NativeInt);
var
  S: String;
  p: PChar;
begin
S := TStringList(AJob.Data).Strings[AIndex];
p := PChar(S);
while p^ <> #0 do
  begin
  if p^ = 'A' then
    AtomicIncrement(FRepeatTimes);
  Inc(p);
  end;
if (AIndex mod 10000) = 0 then
  NotifyProgress(9, AIndex * 100 div MergedCount);
end;

这两个DoSearchChar函数第一个用于触发For并行检查是否包含字母A的作业执行,而作业的进度是每扫描完10000项时,通知一次进度。当然,由于循环是 0~MergedCount-1 ,所以最后一次的进度更新我放到了Button1里去完成。

好了,现在您可以直接跑下例子,看一下程序的运行效果了。

本示例的源码位于 Demos\Delphi\VCL\QWorkerProgress 目录下,你可以直接编译运行。

分享到:

0 条评论

沙发空缺中,还不快抢~