前言
在本章前文所述的各项等待操作中,线程阻塞的时间并不确定,但在实际应用中存在这样的需求:限制线程等待时间——在线程等待特定的一段时间后,开发者既可以向用户发出“该事件仍未完成”的提示,也可以接受由于用户放弃等待从而关闭线程的命令。
判断程序运行超时有两种方式:
- duration-based
该方式需要用户指定一段时间,例如30ms。 - absolute
该方式需要用户提供一个时间点,例如17:30:15.045987023 UTC on November 30, 2011。
大多数等待函数均支持这两种方式,处理duration-based
的后缀为_for
,处理absolute
的后缀为_until
。举例而言,std::condition_variable
的成员函数wait_for()
和wait_until()
。此外,这些函数具备2种重载,第一种重载形式等待着信号触发或虚假唤醒,另一种则在唤醒时检查谓词,并仅在结果为true
的情况下返回,或直接超时返回。更多相关信息可见本节末尾的图表。
clock
在探讨超时之前,有必要先了解一下时间在C++中的表现形式,首先从clock
开始。
对C++标准库而言,clock
意味着时间信息源,具体来说,clock
是一个类,提供了4种不同的信息:
- 当前时间
- 时间类型
- 时钟节拍
- 时钟是否稳定(由时钟节拍确定)
当前时间
当前时间可通过调用该类的静态成员函数now
获得,如std::chrono::system_clock::now()
将返回系统时钟当前时间。
时间类型
时间类型由time_point
中的typedef
指定,因此some_clock::now()
的类型为some_clock::time_point
。
时钟节拍
时钟节拍被指定为1/x(x取决于硬件)秒。若当前时钟一秒有25个节拍,则其一个时钟周期为std::ratio<1, 25>
。若时钟的时钟节拍每2.5秒一次,则时钟周期则可表示为std::ratio<5, 2>
。时钟周期通常会在数据手册中给出,但如果当前并不知悉,则可以通过多次运行特定程序的方法求解平均值(这种方法并不保证精确性)。
时钟稳定性
若时钟节拍均匀分布(无论该速率与时钟周期匹配与否)且无法调整,则称该时钟为稳定时钟,在此类情况下,时钟类中的is_steady
静态数据成员为true
,否则为false
。通常情况下我们可以认为std::chrono::system_clock
不稳定,因为该时钟可被调整(即使这种调整是为了抑制本地时钟漂移而自发进行的)。
对时钟的调整行为可能导致一个问题:当前调用now
返回的时间要早于之前调用now
返回的时间。(举例而言,做手机app的耗时监控,在app启动后用户调整了时间,从而导致计算错误,此时应当使用绝对时间)。
时钟的稳定性对于超时计算而言非常重要,因此C++标准库提供了std::chrono::steady_clock
。std::chrono::system_clock
代表了系统时钟的“实际时间”,并且提供了可将时间点转化为time_t
类型的接口;std::chrono::high_resolution_clock
提供了具备最高分辨率(即最小的时钟周期)的时钟。上述时钟及其相关工具均位于头文件<chrono>
中。
Durations
时延是本节最简单的内容:它们由std::chrono::duration<>
函数模板进行处理(位于std::chrono
命名空间内)。第一个模板参数是一个时延外在表现的类型(如int,long或double),第二个模板参数是制定部分,表示每一个单元所用秒数。举例而言,当存在分钟级时延需要存储于short
类型中时,可以写成std::chrono::duration<short,std::ratio<60,1>>
,因为一分钟包含60秒。若需要将毫秒级时延存储于double
类型中时,则写作std::chrono::duration<short,std::ratio<1,1000>>
,原因在于一秒包含1000毫秒。
在隐式转换下,时延转换不存在截断(即可由时转为秒,但不可由秒转为时)。我们可以通过显式操作std::chrono::duration_cast<>
来完成转换(具备截断):1
2std::chrono::milliseconds ms(54802);
std::chrono::seconds s= std::chrono::duration_cast<std::chrono::seconds>(ms); // s = 54
时延支持算术操作,因此开发者可以对它进行加减乘除操作。1
5*seconds(1) == seconds(5) == minutes(1) - seconds(55)
此外,可以通过成员函数count
获取时延中的单元数量,如std::chrono::milliseconds(1234).count()
是1234。
为了方便起见,在C++1 中引入的std::chrono_literals
命名空间内有许多预定义字面值后缀操作符,因此后续可以直接写为:
1 | using namespace std::chrono_literals; |
具体应用
假设开发者期待在35ms内一个future
对象状态就绪,以便于执行相关操作,那么该需求可被写为:1
2
3std::future<int> f=std::async(some_task);
if(f.wait_for(std::chrono::milliseconds(35)) == std::future_status::ready)
do_something_with(f.get());
所有的wait
函数均返回一个状态,用以指示当前时间已经耗尽或者说等待事件已经发生。在刚才的代码实例中,由于我们调用的是future
的程序函数wait_for
,因此它存在三种返回状态:
- std::future_status::timeout 时延耗尽
- std::future_status::ready future 已就绪
- std::future_status::deferred future 任务被延迟
计算时延所使用的是系统内部提供的稳定时钟,因此无论系统时钟system_clock
发生怎样的调整,线程耗时总是35毫秒。当然,由于难以预料的系统调度和操作系统间不同的时钟精度,从调用该线程到返回该线程的时间可能要远长于35毫秒。
time_point
时钟的时间点可以用std::chrono::time_point<>
类模板实例来表示。模板的第一个参数用来指定所要使用的时钟,第二个参数表示时延的计量单位(std::chrono::duration<>
的特例化 )。一个时间点的值就是以一个时间戳(epoch)作为起点的时间的长度(其必然为指定时延的倍数)。时间戳是时钟的基本属性,但它既不可直接查询也不并非由C++标准指定。通常采用的时间戳为1970年1月1日00:00,或运行应用程序的计算机启动时。不同的时钟实例可能共享一个时间戳,也可能具备各自独立的时间戳。虽然我们并不了解当前使用的是何种时间戳,但开发者可通过对指定time_point
类型使用time_since_epoch()
来获取时延值,该时延值即为当前时间距离时间戳的距离。
具体使用
举例而言,std::chrono::time_point<std:: chrono::system_clock, std::chrono::minutes>
表示当前时间点与系统时钟相关联,且其时延计量单位为分钟。我们可以自由地通过对一个时间点执行算术运算,从而获取另一个时间点,如:1
std::chrono::high_resolution_clock:: now() + std::chrono::nanoseconds(500)
我们也可以在两个时间点之间执行算术运算(二者需要共享一个时钟),从而获取时间差。这常见于代码块的计时需求:1
2
3
4auto start=std::chrono::high_resolution_clock::now();
do_something();
auto stop=std::chrono::high_resolution_clock::now();
std::cout<<”do_something() took “<<std::chrono::duration<double,std::chrono::seconds>(stop-start).count()<<” seconds”<<std::endl;
应当尽量避免在计时代码块中调整std::chrono::time_point
所指定的时钟,因为这将导致计时错误。
假设当前存在一个事件,而我们至多等待该事件500毫秒,则有程序如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
std::condition_variable cv;
bool done;
std::mutex m;
bool wait_loop() {
auto const timeout = std::chrono::steady_clock::now() + std::chrono::milliseconds(500);
std::unique_lock<std::mutex> lk(m);
while(!done) {
if(cv.wait_until(lk,timeout) == std::cv_status::timeout)
break;
}
return done;
}
Functions that accept timeouts
开发者经常需要为特定的线程添加延迟,让它们休眠一段指定时间或休眠至特定时间点,而这两个需求的实现依赖于函数std::this_thread::sleep_for()
与std::this_thread::sleep_until()
。
显然,休眠只是超时(timeout)处理的一种形式,在上文中,超时配合条件变量与期望一起使用。超时甚至可以在尝试获取一个互斥锁时(前提是该互斥锁支持timeout)使用,传统的std::mutex
与std::recursive_mutex
并不支持超时,但std::timed_mutex
与std::recursive_timed_mutex
则可以。它们具备了名为try_lock_for
与try_lock_until
的成员函数。下表则更具体地展示了支持timeout的函数,并给出了它们所需参数与返回值。