关于前后台通讯这一点,我在文章 QWorker技巧之作业与主线程之间通讯 中对此进行了一些说明,但一直没有给出单独的示例。因此在群里,许多朋友对QWorker如何和前台线程通讯还是有点犯迷糊。所以,我特意写了一个小演示程序,来演示如何做到这一点。
这个示例在主线程中是要分别更新10个进度条的进度信息,先看一下截图:
声明下,本示例演示了如何在QWorker中后台作业与界面元素进行交互的基本方法,当然这不是唯一的方法,主要是提供一个参考,更多的方法参考前面提到的文章。
本示例演示了一个三步作业:
- 分别填充8个TStringList对象,并显示填充进度。
- 合并8个TStringList的结点到第一个TStringList
- 在后台线程中遍历合并后的TStringList,来尝试指到特定的字符 A 出现的次数。
本示例用到了以下功能:
- 使用TQJobGroup做了一个串行化作业,保证上面的三步按顺序执行。
- 使用TQWorkers.For做了并行计算,填充8个TStringList列表。
- 使用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 目录下,你可以直接编译运行。