说说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中还有更多的技术亮点,下回分解。

 

posted @ 2014-08-23 13:10  我爱我家喵喵  阅读(1108)  评论(0编辑  收藏  举报