基于 QWorker 的多线程编程 – 线程定时器

在实际编程环境中,我们常需要定时执行一项任务,比如每隔1小时,执行一次同步操作,而这些操作如果放到主线程中使用普通的 TTimer 执行,由于需要占用主线程的资源,可能会造成程序明显的卡顿。这时候,我们就需要使用后台定时器来完成这一操作(当然 QWorker 也支持主线程中的定时作业,此时就相当于你在窗口上放置了一个TTimer 控件)。

QWorker 提供了三种不同的定时方式“

1、一个最普通的定时器,通过 Post 投寄一个带有时间间隔的作业,我们来看对应的 Post 函数的参数(全局函数和匿名函数版本略,下同):

    /// <summary>投寄一个后台定时开始的作业</summary>
    /// <param name="AProc">要执行的作业过程</param>
    /// <param name="AInterval">要定时执行的作业时间间隔,单位为0.1ms,如要间隔1秒,则值为10000</param>
    /// <param name="AData">作业附加的用户数据指针</param>
    /// <param name="ARunInMainThread">作业要求在主线程中执行</param>
    /// <param name="AFreeType">附加数据指针释放方式</param>
    /// <returns>成功投寄返回句柄,否则返回0</returns>
    function Post(AProc: TQJobProc; AInterval: Int64; AData: Pointer;
      ARunInMainThread: Boolean = False;
      AFreeType: TQJobDataFreeType = jdfFreeByUser): IntPtr; overload;

在 Post 时,我们通过 AInterval 参数,指定一个时间间隔就可以达到定时重复作业的目的了。比如下面的代码,每秒会在 IDE 的 Events 调试窗口输出当前时间:

【Delphi】

procedure TForm1.DoTheadTimer(AJob: PQJob);
begin
OutputDebugString(PChar(FormatDateTime('yyyy-mm-dd hh:nn:ss',Now)));
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
Workers.Post(DoTheadTimer,1*Q1Second,nil);
end;

【C++ Builder】

// 头文件部分省略
__fastcall TForm1::TForm1(TComponent* Owner)
	: TForm(Owner)
{
Workers->Post(DoThreadTimer,1*Q1Second,NULL);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::DoThreadTimer(PQJob AJob)
{
OutputDebugString(FormatDateTime("yyyy-mm-dd hh:nn:ss",Now()).c_str());
}

很简单吧!

【注意】 这个定时器不会管你作业是否处理完成,按规定的间隔定时每秒触发1次,如果你长时间做业超时,会造成作业累积,后果可能会~~~~

2、使用 Delay 设置自由定时器

Delay 与 Post 不同,Delay 实际上是一个延迟指定时间执行某一操作的一种作业方式。是一个延迟定时器。由于 QWorker 允许在作业中投寄新作业,因此,在这个作业完成时,再次投寄作业就可以达到重复的目的。这样实现的定时器与第一种方式不同,它实际上类似于我们使用 TTimer 时,在其 OnTimer 事件中先通过设置 Enabled 为 False 禁用 Timer,然后在处理完成后再设置 Enabled 为 True 启用定时器的效果。

我们先看下 Delay 函数的声明:

    /// <summary>投寄一个延迟开始的作业</summary>
    /// <param name="AProc">要执行的作业过程</param>
    /// <param name="AInterval">要延迟的时间,单位为0.1ms,如要间隔1秒,则值为10000</param>
    /// <param name="AData">作业附加的用户数据指针</param>
    /// <param name="ARunInMainThread">作业要求在主线程中执行</param>
    /// <param name="AFreeType">附加数据指针释放方式</param>
    /// <returns>成功投寄返回句柄,否则返回0</returns>
    function Delay(AProc: TQJobProc; ADelay: Int64; AData: Pointer;
      ARunInMainThread: Boolean = False;
      AFreeType: TQJobDataFreeType = jdfFreeByUser): IntPtr; overload;

ADelay 参数决定了作业延迟的时间长度,单位为0.1ms。下面我们就通过Delay做一个随机延迟的Demo,这上作业的内容随机间隔1-3秒,输出一次当前时间:

【Delphi】

procedure TForm1.DoRandomTimer(AJob: PQJob);
var
  ANextDelay:Int64;
begin
OutputDebugString(PChar(FormatDateTime('yyyy-mm-dd hh:nn:ss',Now())));
ANextDelay:=Q1Second+Q1Second*random(300) div 100;
Workers.Delay(DoRandomTimer,ANextDelay,nil);
end;

procedure TForm1.FormCreate(Sender: TObject);
begin
Workers.Delay(DoRandomTimer,1*Q1Second,nil);
end;

【C++ Builder】

__fastcall TForm1::TForm1(TComponent* Owner)
	: TForm(Owner)
{
Workers->Delay(DoRandomTimer,1*Q1Second,NULL);
}
//---------------------------------------------------------------------------
void __fastcall TForm1::DoRandomTimer(PQJob AJob)
{
OutputDebugString(FormatDateTime("yyyy-mm-dd hh:nn:ss",Now()).c_str());
__int64 ANextDelay=Q1Second+Q1Second*random(300)/100;
Workers->Delay(DoRandomTimer,ANextDelay,NULL);
}

3、使用 At 函数在指定的时间点执行

除了上述两种定时作业外, QWorker 还支持定点作业。所谓的定点作业就是在指定的时间点执行的某类作业,比如每天的 9:30 分执行,或者每小时的30分执行。

首先我们来看 At 函数的声明:

    /// <summary>投寄一个在指定时间才开始的重复作业</summary>
    /// <param name="AProc">要定时执行的作业过程</param>
    /// <param name="ADelay">第一次执行前先延迟时间</param>
    /// <param name="AInterval">后续作业重复间隔,如果小于等于0,则作业只执行一次,和Delay的效果一致</param>
    /// <param name="ARunInMainThread">是否要求作业在主线程中执行</param>
    /// <param name="AFreeType">附加数据指针释放方式</param>
    /// <returns>成功投寄返回句柄,失败返回0</returns>
    function At(AProc: TQJobProc; const ADelay, AInterval: Int64;
      AData: Pointer; ARunInMainThread: Boolean = False;
      AFreeType: TQJobDataFreeType = jdfFreeByUser): IntPtr; overload;
    /// <summary>投寄一个在指定时间才开始的重复作业</summary>
    /// <param name="AProc">要定时执行的作业过程</param>
    /// <param name="ATime">执行时间</param>
    /// <param name="AInterval">后续作业重复间隔,如果小于等于0,则作业只执行一次,和Delay的效果一致</param>
    /// <param name="ARunInMainThread">是否要求作业在主线程中执行</param>
    /// <param name="AFreeType">附加数据指针释放方式</param>
    /// <returns>成功投寄返回句柄,失败返回0</returns>
    function At(AProc: TQJobProc; const ATime: TDateTime;
      const AInterval: Int64; AData: Pointer; ARunInMainThread: Boolean = False;
      AFreeType: TQJobDataFreeType = jdfFreeByUser): IntPtr; overload;

第一种形式下,采用的和 Delay 类似的形式,效果等价于 Delay + Post定时重复的效果。首次运行会延迟 ADelay*0.1ms,然后每次按 AInterval 的时间间隔为基准进行重复。如果 AInterval 参数指定为0,则和 Delay 的效果是完全等价的。

第二种形式下,作业首次会在指定的时间点执行,然后再以 AInterval 指定的时间间隔进行重复作业。如果指定的时间已经过了,则计划下一次执行时间。

下面的代码将计划 DoAtJob 作业每天的 9:30 执行一次。

【Delphi】

Workers.At(DoAtJob,EncodeTime(9,30,0,0),Q1Day,nil);

【C++ Builder】

Workers->At(DoAtJob,EncodeTime(9,30,0,0),Q1Day,NULL);

下面是关于 At 函数的一些技巧提示:

1、常见的时间间隔可以直接用 Q1XXXX 乘以数量来计算,如每隔2小时执行一次,则 AInterval 参数为 2*Q1Hour,每隔1周执行一次,则 AInterval 为 7*Q1Day。

2、由于每年、每季度和每月的时长不一样,所以你无法直接用At函数来规划这种作业,这种作业的话,建议你直接用At 单次作业实现,在一次作业完成时,用 IncYear 或 IncMonth 来在指定的时间点计划下一次作业。

好了,下面是关于 QWorker 中时间间隔的一个说明:

QWorker 中时间间隔的单位是 0.1ms,也就是说,在操作系统支持的情况下, QWorker 的作业最快能够 0.1ms 调度1次(如果你的作业执行时间本身就超过 0.1ms 的话就别考虑了),而 QWorker 使用了一个64位整数来对时间间隔进行计数,所以理论上,QWorker 的时间间隔最大约为10675199116天,约合2923年,所以不用担心你的时间溢出的问题。

 

分享到: