《C++并发实例》4.3 限时等待

前面介绍的所有阻塞调用都会无限期地阻塞,挂起线程,直到等待的事件发生。在许多情况下,这没问题,但在某些情况下,您希望限制等待时间。这可能是为了交互用户或另一个进程发送某种形式的“我还活着”消息,或者在实际应用中允许用户放弃等待并按下取消键时中止等待。

有两种您可能希望指定的超时:基于持续时间的超时,您等待特定的时间量(例如 30 毫秒),或绝对超时,您等待直到特定的时间点(例如,2011 年 11 月 30 日 17:30:15.045987023 UTC)。大多数等待函数都提供了处理这两种形式的超时的变体(variants)。处理基于持续时间的超时的变体具有 _for 后缀,处理绝对超时的变体具有 _until 后缀。

因此,例如,关于 std::condition_variable 的 wait() 函数有 wait_for() 和 wait_until()两个重载函数,——一个重载只等待收到信号,或者超时到期,或者发生虚假唤醒,另一个将在唤醒时检查提供的谓词,并且仅当前置条件为真(并且条件变量已发出信号)或超时到期时才会返回。

在详细了解使用超时的函数之前,我们先从时钟开始,先检查一下 C++ 中的时间。

4.3.1 时钟

就 C++ 标准库而言,时钟是一种时间信息来源。特别是,时钟是一个提供四个不同信息的类:
■ 现在的时间
■ 用于表示从时钟获取的时间值的类型
■ 时钟的滴答周期
■ 时钟是否以统一的速率滴答,因而被认为是一个稳定的时钟

可以通过调用该时钟类的静态成员函数now()来获取时钟的当前时间;例如, std::chrono::system_clock::now() 将返回系统时钟的当前时间。特定时钟的时间点类型由 time_point 的成员 typedef 指定,因此 some_clock::now() 的返回类型为 some_clock::time_point

时钟的滴答周期(tick period)被指定为秒的分数,由时钟的 period 成员 typedef 给出——每秒滴答 25 次的时钟因此周期为 std::ratio<1,25 >,而每 2.5 秒滴答一次的时钟的周期为 std::ratio<5,2>。如果时钟的滴答周期要到运行时才能知道,或者它可能在应用程序的给定运行期间发生变化,则该周期可以指定为平均滴答周期、最小可能的滴答周期或其他一些函数库作者认为合适的值。无法保证程序给定运行中观察到的滴答周期与该时钟的指定周期匹配。

如果时钟以统一的速率(无论该速率是否与周期匹配)滴答并且无法调整,则该时钟被称为稳定时钟。如果时钟稳定,则时钟类的 is_steady 静态数据成员为 true,否则为 false。通常, std::chrono::system_clock 不会稳定,因为时钟可以调整,即使这种调整是考虑到本地时钟漂移(local clock drift)自动完成的。这样的调整可能会导致对 now() 的调用返回值早于先前对 now() 的调用返回值,这违反了统一滴答率的要求。稳定时钟对于超时计算非常重要,您很快就会看到,因此 C++ 标准库以 std::chrono::steady_clock 的形式提供了一种时钟。 C++ 标准库提供的其他时钟是 std::chrono::system_clock (上面提到的),它代表系统的“实时”时钟,并提供将其时间点与 time_t 值相互转换的函数,以及 std ::chrono::high_resolution_clock,它提供所有库提供的时钟的尽可能最小的滴答周期(从而提供尽可能高的精度)。它实际上可能是其他时钟之一的类型定义。这些时钟与其他时间工具一起在 库头中定义。

我们很快就会看看时间点的表示,但首先让我们看看持续时间是如何表示的。

4.3.2 持续时间 Durations

持续时间是时间支持中最简单的部分;它们由 std::chrono::duration<> 类模板处理(线程库使用的所有 C++ 时间处理工具都在 std::chrono 命名空间中)。第一个模板参数是表示的类型(例如 int、long 或 double),第二个是一个分数,指定每个持续时间单位表示多少秒。例如,short 中存储的分钟数为 std::chrono::duration<short,std::ratio<60,1>>,因为一分钟有 60 秒。另一方面,存储在 double 中的毫秒计数是 std::chrono::duration<double,std::ratio <1,1000>>,因为每个毫秒是 1/1000 秒。

标准库在 std::chrono 命名空间中提供了一组预定义的类型定义,用于各种持续时间:纳秒、微秒、毫秒、秒、分钟和小时。它们都使用足够大的整数类型来表示所选的表示形式,如果有需要您可以用适当的单位表示 500 年以上的持续时间。还有从 std::atto(10-18) 到 std::exa(1018)(及以上,如果您的平台有 128 位整数类型)的所有 SI 比率的类型定义,可在指定自定义持续时间例如std::duration<double,std::centi> 用于以双精度表示的 1/100 秒计数。

持续时间之间的转换是隐式的,不需要截断值(因此可以将小时转换为秒,但将秒转换为小时则不行)。可以使用 std::chrono::duration_cast<> 完成显式转换:

std::chrono::milliseconds ms(54802);
std::chrono::seconds s=
    std::chrono::duration_cast<std::chrono::seconds>(ms);

结果被截断而不是四舍五入,因此在此示例中 s 的值为 54。持续时间支持算法,因此您可以添加和减去持续时间以获得新的持续时间,或者乘以或除以基础表示类型的常量(第一个模板参数)。因此 5*seconds(1) 与 seconds(5) 或 minutes(1) – seconds(55) 相同。持续时间内的单位数计数可以通过count() 成员函数获得。因此std::chrono::milliseconds(1234).count() 是 1234。

基于持续时间的等待是通过 std::chrono::duration<> 的实例完成的。例如,您最多可以等待 35 毫秒让 future 准备就绪:

std::future<int> f=std::async(some_task);
if(f.wait_for(std::chrono::milliseconds(35))==std::future_status::ready)
    do_something_with(f.get());

等待函数都返回一个状态来表示等待是否超时或等待事件发生。在这个例子中,您正在等待 future,因此如果等待超时,该函数将返回 std::future_status::timeout ;如果 future 已准备好,则返回 std::future_status::ready ;或者 std::future_status::deferred如果 future 的任务被推迟。基于持续时间(duration-base)的等待时间是使用库内部的稳定时钟来测量的,因此 35 毫秒意味着 35 毫秒的经过时间,即使系统时钟在等待期间进行了调整(向前或向后)。当然,系统调度的变幻莫测和操作系统时钟的不同精度意味着线程发出调用和返回之间的实际时间可能比 35 毫秒长得多。

掌握了持续时间后,我们现在可以继续讨论时间点。

4.3.3 时间点

时钟的时间点由 std::chrono::time_point<> 类模板的实例表示,该模板指定引用的时钟作为第一个模板参数和测量单位(std:: chrono::duration<>) 作为第二个模板参数。时间点的值是指从时钟纪元(epoch)到现在的时间长度(大多数的持续时间)。时钟的纪元是一个基本属性,但不能直接查询或由 C++ 标准指定。典型的纪元包括 1970 年 1 月 1 日的 00:00 以及运行应用程序的计算机启动的瞬间。时钟可以共享一个纪元或具有独立的纪元。如果两个时钟共享一个纪元,则一个类中的 time_point 类型定义指定另一个 time_point 类型与之关联。虽然你无法确定纪元是什么时候,但你可以调用给定 time_point 的 time_since_ epoch() 。该成员函数会返回时钟纪元到指定时间点的时间长度。

例如,您可以将时间点指定为 std::chrono::time_point<std:: chrono::system_clock, std::chrono::minutes>。这将保持相对于系统时钟的时间,但以分钟为单位测量,而不是系统时钟精度(通常为秒或更小)。

您可以从 std::chrono:: time_point<> 的实例中增减持续时间以生成新的时间点,因此 std::chrono::high_resolution_clock:: now() + std::chrono::nanoseconds(500) 将给你一个未来 500 纳秒的时间。当您知道运行代码块的最大持续时间时,计算一个固定时间非常有用,但代码中有很多等待函数或者需要时间开销都非等待函数。

您还可以从同一共享时钟的时间点中减去一个时间点。结果是指定两个时间点之间的持续时间长度。这对于计时代码块很有用,例如:

auto 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<> 实例的时钟参数不仅仅指定纪元。当您将时间点传递给采用绝对超时的等待函数时,该时间点的时钟参数用于测量时间。当时钟更改时,这会产生重要的后果,因为等待会根据时钟更改,并且直到时钟的 now() 函数返回晚于指定超时的值时才会返回。如果时钟向前调整,这可能会减少等待的总长度(通过稳定时钟测量),如果向后调整,这可能会增加等待的总长度。

正如您所预见的,时间点与等待函数的 _until 变体一起使用。典型的用例是作为程序中某个固定点的 some-clock::now() 的偏移量,尽管时间点和系统时间可以通过 time_t 的std::chrono::system_clock::to_time_point() 固定成员函数转换成用户可见的时间。例如,如果条件变量关联的等待事件的时间最长为 500 毫秒,您可以执行如下列表所示的操作。

Listing 4.11 Waiting for a condition variable with a timeout

#include <condition_variable>
#include <mutex>
#include <chrono>
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;
    }
}

如果您不向等待函数传递预期时间,这是在有限时间内等待条件的推荐方法。这样,循环的总长度就受到限制。正如您在第 4.1.1 节中看到的,如果不传入预期,则在使用条件变量时需要循环,以处理虚假唤醒。如果您在循环中使用 wait_for(),您可能会在虚假唤醒之前等待几乎整个时间长度,然后开启下一个等待时间。这可能会重复任意多次,从而使总等待时间不受限制。

掌握了指定超时的基础知识后,我们来看看可以使用超时的函数。

4.3.4 接收超时的函数

超时最简单的用途是为特定线程的处理添加延迟,这样当它无事可做时就不会占用处理时间。您在第 4.1 节中看到了一个这样的示例,其中您在循环中获取了“done”标志。处理这个问题的两个函数是 std::this_thread::sleep_ for() 和 std::this_thread::sleep_until()。它们的工作方式就像一个基本的闹钟:线程在指定的持续时间内(使用 sleep_for())或直到指定的时间点(使用 sleep_until())进入睡眠状态。 sleep_for() 对于第 4.1 节中的示例是有意义的,其中任务必须定期完成,并且所用时间才是重要的。另一方面,sleep_until() 允许您安排线程在特定时间点唤醒。这可用于在午夜触发备份,或在早上 6:00 运行工资单打印,或者在进行视频播放时暂停线程直到下一帧刷新。

当然,睡眠并不是唯一需要超时的方式。您已经看到可以将超时与条件变量和 future 一起使用。如果互斥体支持,您甚至可以在尝试获取互斥体上的锁时使用超时。普通 std::mutex 和 std::recursive_mutex 不支持锁定超时,但 std::timed_mutex 支持,std::recursive_timed_mutex 也支持。这两种类型都支持 try_lock_for() 和 try_lock_until() 成员函数,尝试在指定时间段内或指定时间点之前获取锁。表 4.1 显示了 C++ 标准库中可以接受超时的函数、它们的参数和返回值。列为duration 的参数必须是std::duration<> 的实例,列为time_point 的参数必须是std::time_point<> 的实例。


Table 4.1 Functions that accept timeouts

现在我已经介绍了条件变量、futures、promise 和打包任务的机制,是时候看看更广阔的前景以及如何使用它们来简化线程之间操作的同步。

posted @ 2024-10-03 21:45  李思默  阅读(149)  评论(0)    收藏  举报