说说QWorker作业Clear与JobGroup超时机制
QWorker来自于QDAC项目,是QDAC作者swish创作的一件神器,它是专门用来进行作业管理的轻量级类库。说是轻量级,只是从代码结构上来讲,功能可一点也不弱。QWorker的用法很简单,swish也写了相关的博文,不会用的同学可以自己找来浏览即可。
QWorker可以提供了清除正在执行和未执行Job(作业)的功能,以及JobGroup超时机制。那么,它是怎么实现Clear与Timeout的呢?今天我想说的正是这个。
在TQWorkers中,提供了5个Clear函数重载,可以用来清除全部作业或指定的作业。我们先来看看Clear全部作业的实现:
procedure TQWorkers.Clear; var i: Integer; AParam: TWorkerWaitParam; ASignal: PQSignal; begin DisableWorkers; // 避免工作者取得新的作业 try FSimpleJobs.Clear; FRepeatJobs.Clear; FLocker.Enter; try for i := 0 to FSignalJobs.BucketCount - 1 do begin if Assigned(FSignalJobs.Buckets[i]) then begin ASignal := FSignalJobs.Buckets[i].Data; FreeJob(ASignal.First); ASignal.First := nil; end; end; finally FLocker.Leave; end; AParam.WaitType := $FF; WaitRunningDone(AParam); finally EnableWorkers; end; end;
Clear方法首先就调用了DisableWorkers,通过对内部计数器FDisableCount加1后,标示工作者不要再来获取新的作业了。然 后就是分别调用FSimpleJobs(简易的作业)和FRepeatJobs(带触发时间控制的作业)的Clear方法来清除各自的作业后,再用For 循环来清除使用Hash表记录的由信号激活的等待型作业。在前面所清除的,实际上都是还未执行,正在等待的作业,接下来,通过 WaitRunningDone来停止正在运行的作业。可以看到,在WaitRunningDone之前,有一句:
AParam.WaitType := $FF;
WaitRunningDone(AParam);
这里的$FF,是用来标示,本次调用WaitRunningDone是要清除全部作业。其它的4个Clear与清除全部作业的Clear相似,只不过多 了几个参数,用来清除指定的作业。比如“Clear(AObject: Pointer; AMaxTimes: Integer)”(后面暂时叫Clear Object),就是用来清除与指定对象AObject关联的作业,AMaxTimes则是指定最大清除的数量,小于0则全部相关的都被清除,这些都可以 从源码的详细注释中看到。Clear Object与Clear执行过程差不多,先是调用了FSimpleJobs的Clear中对于Clear Object的重载函数,记录清除数量,然后再调用FRepeatJobsr的,接着就是ClearSignalJobs函数用来清除 FSignalJobs中的相关作业。这三步完成后,判断AMaxTimes是否等于0,以便确定是不是要再次去停止正在执行的作业。注意的是,这里为什 么要判断AMaxTimes不等于0呢?从源码中可以看到,每次清除完成后都会:
Dec(AMaxTimes, ACleared);
如果参数AMaxTimes大于0呢,在执行到WaitRunningDone之前,肯定就会是 >= 0, 大于0表示没有达到指定的最大数量。至于参数AMaxTimes本身小于0的时候,那到WaitRunningDone之前, 也肯定不会等于0了。所以这里判断<>0,就可以知道要不要执行WaitRunningDone。
前面多次说到了WaitRunningDone,它是干什么的呢? 从字面意思可以猜到,它是用来等待运行中的作业完成的。让我们来看看WaitRunningDone的实现:
procedure TQWorkers.WaitRunningDone(const AParam: TWorkerWaitParam); var AInMainThread: Boolean; function HasJobRunning: Boolean; var i: Integer; AJob: PQJob; begin Result := False; DisableWorkers; FLocker.Enter; try for i := 0 to FWorkerCount - 1 do begin if FWorkers[i].IsLookuping then // 还未就绪,所以在下次查询 begin Result := true; Break; end else if FWorkers[i].IsExecuting then begin AJob := FWorkers[i].FActiveJob; case AParam.WaitType of 0: // ByObject Result := TMethod(FWorkers[i].FActiveJobProc).Data = AParam.Bound; 1: // ByData Result := (TMethod(FWorkers[i].FActiveJobProc) .Code = TMethod(AParam.WorkerProc).Code) and (TMethod(FWorkers[i].FActiveJobProc) .Data = TMethod(AParam.WorkerProc).Data) and ((AParam.Data = Pointer(-1)) or (FWorkers[i].FActiveJobData = AParam.Data)); 2: // BySignalSource Result := (FWorkers[i].FActiveJobSource = AParam.SourceJob); 3: // ByGroup Result := (FWorkers[i].FActiveJobGroup = AParam.Group); $FF: // 所有 Result := true; else raise Exception.CreateFmt(SBadWaitDoneParam, [AParam.WaitType]); end; if Result then begin FWorkers[i].FTerminatingJob := AJob; Break; end; end; end; finally FLocker.Leave; EnableWorkers; end; end; begin AInMainThread := GetCurrentThreadId = MainThreadId; repeat if HasJobRunning then begin if AInMainThread then begin // 如果是在主线程中清理,由于作业可能在主线程执行,可能已经投寄尚未执行,所以必需让其能够执行 {$IFDEF NEXTGEN} fmx.Forms.Application.ProcessMessages; {$ELSE} Forms.Application.ProcessMessages; {$ENDIF} end; {$IFDEF MSWINDOWS} SwitchToThread; {$ELSE} TThread.Yield; {$ENDIF} end else // 没找到 Break; until 1 > 2; end;
WaitRunningDone有一个TWorkerWaitParam参数,它决定了WaitRunningDone要等待什么。比如当 AParam.WaitType=$FF时,是要清除所有Job,这里要作的就是等待所有正在运行的作业结束。当AParam.WaitType=3时, 则是等待一个作业Group结束。WaitRunningDone首先判断是不是调用是不是来自主线程,以便在后面的等待过程中作出相应的处理。然后就是 循环调用HasJobRunning子函数,判断是不是还有符合条件的作业正在运行。如果有就进行消息处理(主线程调用时)或 Yield 转让处理器时间片给别的线程,然后继续等待,直到再也没有正在运行的Job。在HasJobRunning中,使用For循环来判断每一个Worker工 作者是不是有满足条件的作业正在运行。碰到包含有IsLookuping标志(还未就绪,即将运行)的工作者是,等待它运行。含有IsExecuting 标志的工作者表示已经在运行了,通过参数检测是否有满足条件的作业,有的话,设置FTerminatingJob为这个工作者当前正在运行的 FActiveJob。FTerminatingJob只会被工作者自己读,这里写入并不存在冲突,可以认为是线程安全的。设置 FTerminatingJob后,Job的IsTerminated将会返回True,这样作业就可以自己退出运行。
通过上面的处理,QWorker就完美的实现了Clear作业的功能,包括普通作业,定时作业,长时间作业,作业组等。
接下来说说TQJobGroup,它提供了Cancel方法用于清除还未执行的Job,Run方法也提供了一个Timeout参数用来作超时处理。TQJobGroup是怎么实现Cancel也超时机制的呢?先看Cancel的代码:
procedure TQJobGroup.Cancel; var i: Integer; AJobs: TQSimpleJobs; AJob, APrior, ANext: PQJob; AWaitParam: TWorkerWaitParam; begin FLocker.Enter; try if FByOrder then begin for i := 0 to FItems.Count - 1 do begin AJob := FItems[i]; if AJob.PopTime=0 then Workers.FreeJob(AJob); end; end; FItems.Clear; finally FLocker.Leave; end; // 从SimpleJobs里清除关联的全部作业 AJobs := Workers.FSimpleJobs; AJobs.FLocker.Enter; try AJob := AJobs.FFirst; APrior := nil; while AJob <> nil do begin ANext := AJob.Next; if AJob.IsGrouped and (AJob.Group = Self) then begin if APrior = nil then AJobs.FFirst := AJob.Next else APrior.Next := AJob.Next; AJob.Next := nil; Workers.FreeJob(AJob); if AJob = AJobs.FLast then AJobs.FLast := nil; end else APrior := AJob; AJob := ANext; end; finally AJobs.FLocker.Leave; end; AWaitParam.WaitType := 3; AWaitParam.Group := Self; Workers.WaitRunningDone(AWaitParam); end;
Cancel先是对自己的作业队列锁定,将里面还没有被Pop的作业释放掉。这里使用了PopTime=0来判断是否已经被Pop。完成后清空整个 Items。为什么要判断PopTime=0?这是因为作业Pop之后,并不会从Items中移除,没这个必要。在将未Pop的作业清理掉后,从 SimpleJobs里清除关联的全部作业。这里从SimpleJobs中清除关联作业,是因为JobGroup的作业,在Pop后就会进入 SimpleJobs的队列中。清除SimpleJobs中的关联作业时,判断每一个Job的IsGrouped标志,以及Job.Group = Self,确定是正要清除的JobGroup中的作业。SimpleJobs队列中的作业也前面Clear时的一样,都是还在等着执行的作业,所以这里直 接FreeJob。这一步完成后,初始化一个WaitType=3,Group=Self的等待参数,去调用Workers的 WaitRunningDone,清理符合条件的已经在执行的作业。此处WaitRunningDone与Clear的运作一样,只不过WaitType 变成了3,表示等待的是一个作业组的作业运行完成。
除了Cancel可以取消JobGroup中还未被执行完的作业外,Run的Timeout参数提供了超时机制,Timeout > 0时,超时机制生效。有了Cancel后,超时的实现就很容易了:
procedure TQJobGroup.Run(ATimeout: Cardinal); var i: Integer; begin if AtomicDecrement(FPrepareCount) = 0 then begin if ATimeout <> INFINITE then begin FTimeout := GetTimestamp - ATimeout; Workers.Delay(DoJobsTimeout, ATimeout * 10, nil); end; ... Run ... if FWaitResult <> wrIOCompletion then DoAfterDone; end; end; procedure TQJobGroup.DoJobsTimeout(AJob: PQJob); begin Cancel; if FWaitResult = wrIOCompletion then begin FWaitResult := wrTimeout; FEvent.SetEvent; DoAfterDone; end; end;
从上面的代码,可以看到,当ATimeOut <> -1 时,JobGroup只是给Workers添加了一个Delay延时执行DoJobsTimeOut的任务。等着时间到达时执行 DoJobsTimeOut。在DoJobsTimeout中调用了Cancel方法,并判断WaitResult(等待返回事件)是不是等于 wrIOCompletion,确定DoJobsTimeout是否有被调用过(可能作业在超时前已经执行完了,或者已经被Cancel了),没有则设置 WaitResult := wrTimeout,然后DoAfterDone执行作业完成事件通知。DoAfterDone在作业完成、超时、取消时,都会触发。
到此,本文就结束了,QWorker中还有更多的技术亮点,下回分解。