平时编写程序时,我们并不总是希望代码一口气以最快速度跑完。比如制作每秒跳动一次的倒计时、每隔 500 毫秒检查一次设备状态,或者在网络请求失败后等待 2 秒再重试,都需要让程序主动 “暂停” 一小段时间。
在早期的 C/C++ 代码中,延时函数在不同操作系统下的写法和单位都不一样,比如 Windows 下调用 Sleep() 按毫秒计时,Linux 下调用 sleep() 按秒计时。从 C++11 开始,标准库在 <thread> 头文件中提供了 sleep_for() 和 sleep_until() 函数,配合前面学过的 chrono 时间单位就能写出跨平台的延时代码。
暂停指定时长:sleep_for()
在 C++ 中,绝大多数延时需求都是 “让程序暂停多长时间”,比如暂停 1 秒或 500 毫秒。遇到这类场景,我们直接调用 std::this_thread::sleep_for() 函数就可以了。
语法:
std::this_thread::sleep_for(时间段);说明:
调用该函数需要同时引入 <thread> 和 <chrono> 两个头文件。其中 this_thread 表示当前正在运行的线程,关于多线程的具体概念,后面章节会详细介绍,这里我们先把它当成固定前缀直接调用即可。
执行 sleep_for() 后,程序就会停在这一行不再往下走,直到括号里指定的 duration 时间段过去之后,才恢复执行后面的代码。常见的时间段写法有表示 1 秒的 std::chrono::seconds{1} 和表示 500 毫秒的 std::chrono::milliseconds{500}。
示例 1:暂停 500 毫秒
#include <chrono>
#include <iostream>
#include <thread> // 提供 std::this_thread::sleep_for()
int main() {
std::cout << "开始等待...\n";
// 让程序暂停 500 毫秒
std::this_thread::sleep_for(std::chrono::milliseconds{500});
std::cout << "500 毫秒已到,继续执行!\n";
return 0;
}运行结果如下。
开始等待...
500 毫秒已到,继续执行!分析:
在这个例子中,程序打印完第 1 句话后,遇到 std::this_thread::sleep_for(...) 就会原地停顿半秒钟,接着再打印第 2 句话。小伙伴们在本地运行时,可以把括号里的 500 改成 2000 试一下,就能直观感受到停顿时间变长了。
配合前面学过的时间字面量后缀,我们还可以把暂停时长写得更简短直观。对于上面这个例子来说,下面 2 种方式是等价的。
// 方式 1
std::this_thread::sleep_for(std::chrono::milliseconds{500});
// 方式 2
using namespace std::chrono_literals;
std::this_thread::sleep_for(500ms);这里顺便提一点:sleep_for() 保证的是 “至少暂停这么久”。如果我们按照前面计算程序运行时间的方法去测量暂停前后的实际耗时,会发现暂停 500 毫秒实际可能经过了 501~505 毫秒。这是因为定时结束后,操作系统重新唤醒程序还需要花费一点调度时间,多出几毫秒属于正常现象。
如果把 sleep_for() 放进 for 循环中,我们就能很方便地做出每秒跳动一次的倒计时效果。下面我们来看一个例子。
示例 2:模拟 3 秒倒计时
#include <chrono>
#include <iostream>
#include <thread>
int main() {
for (int i = 3; i >= 1; i--) {
std::cout << "倒计时:" << i << " 秒\n";
std::this_thread::sleep_for(std::chrono::seconds{1});
}
std::cout << "发射!\n";
return 0;
}运行结果如下。
倒计时:3 秒
倒计时:2 秒
倒计时:1 秒
发射!分析:
在 for 循环内部,每次打印完当前秒数后,程序都会通过 std::this_thread::sleep_for(std::chrono::seconds{1}); 暂停 1 秒钟。当小伙伴们在本地运行这段代码时,可以看到屏幕每隔 1 秒打印一行倒计时,停顿 3 次后输出 "发射!"。
需要注意的是,平时千万不要自己写一个几亿次的空循环来凑延时效果。空循环不仅在不同快慢的电脑上耗时完全不可控,还会在等待期间白白占满 CPU 资源;而调用 sleep_for() 暂停时,程序会主动让出 CPU,等待期间几乎不消耗计算资源。
提示 如果我们想在同一行连续打印 3... 2... 1... 而不加换行符 \n,在某些终端里可能会发现文字没有逐秒跳出,而是等 3 秒结束后才一次性显示出来。这是因为未换行的内容还暂存在输出缓冲区里,我们只要在输出语句末尾加上 << std::flush 立即刷新缓冲区,就能看到逐秒跳动的效果了。
暂停到指定时间点:sleep_until()
除了指定 “暂停多久” 的 sleep_for() 之外,<thread> 头文件中还提供了配套的 std::this_thread::sleep_until() 函数,用来让程序一直暂停到某一个具体的 time_point 时间点。
语法:
std::this_thread::sleep_until(目标时间点);说明:
sleep_until() 接收的参数不是时间段,而是具体的 time_point 时间点。比如我们先获取当前时间点,加上 300 毫秒算出未来的目标时刻 wakeTime,再把 wakeTime 传给 sleep_until(),程序就会一直休息到那个时刻才恢复执行。
那它和 sleep_for() 在实际用途上有什么区别呢?假设我们要每隔 1 秒执行一次数据采集,而采集任务本身需要消耗 150 毫秒。如果每次采集完都固定调用 sleep_for(1s),实际周期就会变成 1.15 秒,跑久了节奏会越来越慢。
如果改用 sleep_until() 并每次把目标时刻固定往后推 1 秒,情况就不一样了。它会自动扣掉任务自身消耗的 150 毫秒,只休息剩下的 850 毫秒,让每一次触发都准确卡在整秒节奏上。
示例 3:暂停至目标时间点
#include <chrono>
#include <iostream>
#include <thread>
int main() {
using namespace std::chrono;
// 计算 300 毫秒后的目标时间点
auto wakeTime = steady_clock::now() + milliseconds{300};
std::cout << "进入等待...\n";
// 暂停至目标时间点
std::this_thread::sleep_until(wakeTime);
std::cout << "已到达目标时间点,继续执行!\n";
return 0;
}运行结果如下。
进入等待...
已到达目标时间点,继续执行!分析:
在这个例子中,我们先通过 steady_clock::now() + milliseconds{300} 算出 300 毫秒之后的目标时间点 wakeTime,再调用 sleep_until(wakeTime) 让程序一直暂停到该时刻。推算唤醒时刻时推荐优先搭配 steady_clock,避免等待期间系统时间发生跳变而影响等待时长。
在实际开发中,普通延时等待、简单倒计时或失败重试直接使用传入 “时间段” 的 sleep_for() 即可;只有需要对齐某个固定截止时刻或按固定周期触发任务时,才使用传入 “时间点” 的 sleep_until()。小伙伴们在调用时要注意,不要把两者的参数类型传反了,否则会直接引发编译报错。
using namespace std::chrono;
auto tp = steady_clock::now() + milliseconds{500};
// 正确方式
std::this_thread::sleep_for(milliseconds{500});
std::this_thread::sleep_until(tp);
// 错误方式
std::this_thread::sleep_for(tp);
std::this_thread::sleep_until(milliseconds{500});