<>引子

  因为在项目中有很多定时的任务,而这些任务一般又是比较费时的任务,所以不方便在主线程中用窗口定时器来实现,因为这样有可能会阻塞界面,从而导致界面卡顿。另外,用窗口定时器也导致逻辑代码与界面代码耦合。所以实现了一个简单的定时任务泵,其实现原理是这样的。

  首先,封装了一个任务基类,其中有一个Do虚方法。各种实际任务从这个类派生特定的任务子类,并实现Do方法。

  然后,实现了一个定时任务管理类。一方面,它用一个std::vector容器来存放各种任务的一个指针并管理这些任务的释放,有一个添加任务的AddTask
接口负责添加任务。再一方面,它会创建一个线程来遍历这个std::vector取出并执行这些任务,并在接到关闭请求后通知这个线程退出,有一个Startup
启动线程的接口和一个Terminate
关闭线程的接口。在线程里,我们是用Sleep系统调用来实现定时的,也就是如果一个任务要5分钟执行一次,我们就每执行一次后让线程Sleep一个5分钟的时间,如此循环。由于要保证退出时的响应性,所以一次Sleep太久是不友好的。所以是让它每次Sleep的时间是1毫秒,然后循环睡眠n次,代码如下所示。
... int count = 0; do { ::EnterCriticalSection(&lock_); bool exit = exit_; ::
LeaveCriticalSection(&lock_); if (exit) return false; if (milliseconds <= 0)
break; ::Sleep(1); ++count; } while (count < milliseconds); ...
  这样实现后,在我的机器上测试了并且一切都感觉perfect,于是就分发了。

  过了一阵子后,便有反馈,说一些定时任务并没有执行。得到这个反馈,我是很愤慨的,内心立马就嘀咕着:“我靠,不是在找茬吧,我当初可是测试了的,运行得很perfect呀。”

  于是我又在自己机器上把条件设置成那样运行了一遍,一切还是那样perfect。我就有点生气了,不过我没有爆发,谨慎的把目标机器的运行日志拿了过来,发现确实没有定时任务运行的日志记录。这下子,我感觉有点不妙了,这应该是事实。那台机确实没有perfect的运行。

<>思考

  于是,凭直觉的,我感觉问题应该出在了Sleep(1)上,于是在网上搜索了这个调用,综合了一下,大致把问题是搞明白了。

  其实,这个问题的深度,要深可以深,要浅也可以浅。因为这个问题如果追根究底是可以找到操作系统内核的。

  我当初在网上搜索是搜索到了,说Sleep的精度因系统平台而异,一般是16毫秒左右,不过也可以到1毫秒。也就是说,如果精度是16毫秒,而Sleep(1)
这样的调用是不行的,因为每一次这样的调用误差是16倍,如果一个任务定时是1个小时,那实际就是16个小时,而说不定没到16个小时软件就可能被重启了,所以定时任务会得不到执行。而如果精度是1毫秒,那么就会运行得比较perfect,就如同我的机器上那样。如果从浅里讲,原因到这儿也就over了。

  但是如果还不满意这样的解释,也是可以更进一步的。这就涉及到操作系统内核的知识了。其实操作系统对于所有的定时的实现是依赖于硬件时钟的,线程的切换也是跟这相关的。这个所谓的硬件时钟实现了一个时间中断,每隔一定的时间会去打断CPU一下,让CPU中断现在执行的东西而去执行与定时相关的东西,比如检查某个定时是不是到期了该触发了。
Sleep本质上也用到了定时,所以也与这个硬件时钟有关。而上面所说的精度便是指这个硬件时钟发出这样的时间中断的间隔时间。

  因为到期的检查是以一定的间隔来检查的。如果定时的时间小于这个检查时间间隔,于是便有误差产生了。比如,1毫秒的定时时间,16毫秒检查一次有没有到期。如果这个定时调用
Sleep(1)离即将到来的最近的一次检查的时间距离小于1毫秒,则这次不会检查到到期,到下一次才会检查到,从而就产生了一个16+毫秒的实际定时。如果这个定时调用
Sleep(1)
离即将到来的最近的一次检查的时间距离大于等于1毫秒,可能就在最近的一次检查就到期了,从而就产生一个16-毫秒的实际定时,比上一种情况好那么一点,但是也是有误差的。

  当然,如果这个硬件时钟的中断时间是1毫秒,也就是定时是1毫秒检查一次,那么,基本上就不会出现上面的情况,也就基本没有误差了。这便是这个问题的深层次的解释。

<>方案

  知道了原因是不够的,还要有解决方案,这个问题的根源其实就是这个硬件时钟的中断时间太大,如果调小一点不就OK了。好在,Windows操作系统上确实是可以调这个中断时间的。这个系统调用便是
timeBeginPeriod,当然还有一个与之对应的timeEndPeriod系统调用。timeBeginPeriod
用来请求系统设置一个时间间隔,最小可以是1毫秒,而timeEndPeriod
用来取消请求的设置。需要注意的是,这个设置是操作系统全局的,所有的进程以及驱动程序共享的,系统会以最高的精度为准,因为只要满足了最高精度,便满足了所有的精度要求。如果进程没有调用
timeEndPeriod
,操作系统会在这个进程退出时取消之前请求的设置。当最高精度的设置请求全部被取消时,操作系统会用次高精度的设置来重新设置。如果所有精度请求都取消了,系统会把这个时钟间隔恢复到16毫秒左右。

  当然,这个任务泵的实现其实可以不用Sleep来实现的,用WaitForSingleObject+SetEvent
会更好,这个调用在等待的同时还会检测一个事件是否触发,这个事件就是通知线程退出的事件,这样就可以绕过对于Sleep(1)的需要了。

  如果一定要用Sleep(1)的话,可以如下这样用来避免对于Sleep精度的依赖:
... DWORD tick_count = ::GetTickCount(); while (true) { ::EnterCriticalSection(
&lock_); bool exit = exit_; ::LeaveCriticalSection(&lock_); if (exit) return
false; if (milliseconds <= 0) break; DWORD current_tick_count = ::GetTickCount()
; if (tick_count < current_tick_count) { if (current_tick_count - tick_count <
milliseconds) { ::Sleep(1); } else { break; } } else { if (0xffffffff -
tick_count+ current_tick_count < milliseconds) { ::Sleep(1); } else { break; } }
} ...

技术
下载桌面版
GitHub
百度网盘(提取码:draw)
Gitee
云服务器优惠
阿里云优惠券
腾讯云优惠券
华为云优惠券
站点信息
问题反馈
邮箱:ixiaoyang8@qq.com
QQ群:766591547
关注微信