这是一个好问题,在多线程、多协程之中,确保获取时间不会出现问题的好办法,大约有两类:
1、每次通过系统调用从OS之中获取实时的系统时间,但这会增加更多的系统调用,非常影响程序的执行能效的。
不少人认为这无伤大雅,在密集的计算程序之中,当执行的频率足够高时,它所带来的代价会变得无限大。
在 Windows 平台上,可以通过PDH性能计数器 QueryPerformanceCounter 来获取过去的时间,效率相对高效,因为它并不需要步入内核,但这不意味着大量频繁的函数调用,它不会带来额外且昂贵的CPU计算开销,并且大多数应用程序并不需要那么高的精度。
每10个毫秒、100个毫秒更新一次时间精度,这块其实就已经很充裕了。
2、定时的时间更新,并在应用程序的:静态变量、全局变量之中进行缓存,但这会带来本文标题所指示的:致命的时间片问题。
那么为什么会产生这样致命的时间片问题呢?
假设我们在一个后台线程之中更新时间,中间无论是否该线程是不停止死循环空转CPU的执行,还是定时延迟(sleep)后在执行时间的更新。
都会在多线程、多协程这类程序之中,产生时间片获取过于老旧的问题,在大多数时,它或许并不会产生问题,但在某些情况下,它可能发生致命且难以被追踪排查的疑难问题。
比如:
我们有一个对象,大约为五秒钟过期保活时间,当对象产生某些行为时,更新它本身的计数时间,并从此重新计数到五秒后在过期保活,并自动被释放。
似乎根据定时的时间片更新,活动它并不会导致该程序出现预料之外的问题,但事实是它还真会,这会出现于这样的时间判断检查表达式之中。
伪代码:
now = now_time();
last = 对象->last_;
if (last > now || now >= (last + 5)) {
// 执行释放。
}
上述代码会存在致命的问题,last > now 这样的表达式,本意是好的,因为:时间可能发生算术溢出的现象。
所以:如果发生了溢出,就兼容并尝试处理它。
但这在第二类的场景之中,几乎是致命性的,除非进程本身是单线程运行,并在每个循环的最顶部更新时间,否则几乎不可避免此类问题的发生。
因为:last 还真有可能会比 now 更大,这是内核调度线程或应用程序调度协程产生的流程执行顺序导致的时间并行安全问题。
那么解决该问题的办法是什么?
1、最简单的,删除 last > now 这样画蛇添足的兼容性表达式,并把时间从32位修改为64,用64位无符号整数表示。
2、较复杂的,删除 last > now 这样的表达式,并判断时间环绕,并按照时间环绕来处理实际过去的时间。
我们通过以下的表达式来判断时间是否已经发生了环绕。
static inline bool before(uint32_t seq1, uint32_t seq2) noexcept
{
return (int32_t)(seq1 - seq2) < 0;
}
比如:
if (now < last && before(last, now)) {
// 发生了时间环绕:
}
演示代码(一):
#include <cstdint>
#include <iostream>
static inline bool before(uint32_t seq1, uint32_t seq2) noexcept {
return (int32_t)(seq1 - seq2) < 0;
}
int main() {
uint32_t now = 10; // 当前时间
uint32_t last = -1; // 上次时间
if (before(last, now)) {
std::cout << "发生了环绕" << std::endl;
} else {
std::cout << "没有发生环绕" << std::endl;
}
return 0;
}
演示代码(二):
#include <cstdint>
#include <iostream>
static inline bool before(uint32_t seq1, uint32_t seq2) noexcept {
return (int32_t)(seq1 - seq2) < 0;
}
int main() {
uint32_t now = 10; // 当前时间
uint32_t last = -1; // 上次时间
if (before(last, now)) {
std::cout << "环绕:过去时间:" << ((uint64_t)now + UINT32_MAX) - last << std::endl;
} else {
std::cout << "不环:过去时间:" << now - last << std::endl;
}
return 0;
}