C---标准库快速参考-三-

133 阅读48分钟

C++ 标准库快速参考(三)

原文:C++ Standard Library Quick Reference

协议:CC BY-NC-SA 4.0

七、并发

线程<thread>

线程是能够编写并行运行的代码的基本构件。

启动新线程

要在新的执行线程中运行任何函数指针、仿函数或 lambda 表达式,请将它传递给std::thread的构造函数,以及任意数量的参数。例如,这两行是等价的:

std::thread worker1(function, "arg", anotherArg);
std::thread worker2([=] { function("arg", anotherArg); });

在从thread的构造函数返回之前,带有参数的函数在新启动的执行线程中被调用。

函数及其参数必须首先被复制或移动(例如,对于临时对象或如果使用了std::move())到这个新线程可访问的内存中。因此,要将引用作为参数传递,首先必须使其可复制:例如,使用std::ref() / std::cref()包装它。当然,您也可以简单地使用带有引用捕获的 lambda 表达式。函子、引用包装器和 lambda 表达式都将在第二章中详细讨论。

thread类不提供任何检索函数结果的工具。相反,它的返回值被忽略,如果它引发了一个未被捕获的异常,就调用std::terminate()(默认情况下会终止进程:参见第八章)。通过使用在<future>头文件中定义的结构,检索函数结果变得更加容易,这将在本章后面详述。

Tip

为了异步执行一个函数并在以后检索它的结果,推荐使用std::async()(在<future>中定义)而不是thread。这通常更容易也更有效(实现async()可能使用线程池)。为不一定返回结果的长期运行的并发任务保留线程的使用。

线程的生存期

如果一个std::thread与一个执行线程相关联,那么它就是可接合的。使用用函数 start out joinable 初始化的joinable(). thread来查询该属性,而默认构造的函数 start out non-joinable。之后,线程实例可以按预期移动和交换。然而,复制thread对象是不可能的。这确保了在任何时候,最多一个thread实例代表一个给定的执行线程。底层本地线程表示的句柄可以通过可选的native_handle()成员获得。

关于std::thread s,需要记住的两个最重要的事实如下:

  • 即使在线程函数执行完毕后,thread仍保持可连接状态。
  • 如果一个thread对象在被析构时仍然是可连接的,那么从它的析构函数中调用std::terminate()

因此,为了确保后一种情况不会发生,一定要确保最终在每个可连接的thread上调用以下函数之一:

  • join():阻塞,直到线程函数执行完毕
  • detach():解除thread对象与可能继续执行的线程的关联

注意,分离thread是以一劳永逸的方式异步执行函数的唯一标准方式。

std::thread没有提供终止、中断或恢复底层执行线程的方法。因此,停止线程功能或与之同步必须使用其他方法,如互斥或条件变量,这两种方法将在本章后面讨论。

线程标识符

每个活动线程都有一个唯一的thread::id,它提供了线程标识符通常需要的所有操作:

  • 它们可以输出到字符串流(例如,用于日志记录)。
  • 可以使用==对它们进行比较(例如,测试/断言某个函数在某个特定线程上执行)。
  • 它们可以在有序和无序的关联容器中用作键:所有的比较操作符(<>=等等)都被定义,就像std::hash()的专门化一样。

如果一个std::thread对象是可连接的,你可以在它上面调用get_id()来获得相关线程的标识符。所有不可连接的thread都有一个等同于默认构造的thread::id的标识符。要获取当前活动线程的标识符,还可以调用全局std::this_thread::get_id()函数。

实用功能

静态std::thread::hardware_concurrency()函数返回当前硬件支持的并发线程数(或其近似值),如果无法确定,则返回零。这个数字可能大于物理内核的数量:例如,如果硬件支持同步多线程(英特尔称之为超线程),这将是内核数量的偶数倍(通常是两倍)。

除了get_id()std::this_thread名称空间包含三个额外的函数来操纵当前的执行线程:

  • yield()提示实现重新调度,允许其他活动线程继续执行。
  • sleep_for(duration)sleep_until(time_point)暂停当前线程一段时间或直到给定时间;使用第二章中描述的<chrono>中的类型指定超时。

例外

除非在此注明,否则<thread>中的所有函数都声明为noexcept。几个std::thread成员调用本地系统函数来操纵本地线程。如果这些失败,则抛出一个std:: system_error并带有以下错误代码之一(参见第八章了解更多关于system_error s 和错误代码的信息):

  • resource_unavailable_try_again如果在构造函数中不能创建新的本机线程
  • invalid_argument如果join()detach()在不可连接的线程上被调用
  • no_such_process如果join()detach()被调用并且线程无效
  • resource_deadlock_would_occur如果从相应的执行线程调用可加入线程上的join()

通过抛出一个std::bad_ alloc的实例或者一个从bad_alloc派生的类,也可以报告在构造函数中分配存储失败。

期货<future>

<future>头提供了从正在、将要或已经执行的函数中检索结果(值或异常)的工具,通常在不同的线程中。从概念上讲,线程安全的通信通道是在单个提供者和一个或多个返回对象(T可能是void或引用类型)之间建立的:

A417649_1_En_7_Figa_HTML.jpg

共享状态是一个内部引用计数对象,在单个提供者和一个或多个返回对象之间共享。提供者异步地将结果存储到它的共享状态中,然后该状态被称为就绪。获得这个结果的唯一方法是通过一个相应的返回对象。

返回对象

所有返回对象都有一个同步的get()函数,该函数会阻塞,直到相关的共享状态就绪,然后或者返回提供的值(可能是void)或者在调用线程中重新抛出提供的异常。

要等到结果准备好而不实际检索它,使用等待函数之一:wait()wait_until(time_point)wait_for(duration)。前者无限期等待,后两者等待的时间不会超过使用<chrono>(第二章中定义的类型之一指定的超时时间。

与共享状态相关联的返回对象被认为是有效的。可以使用valid()检查有效性。有效的future不能直接构造,但必须总是从共享状态的单一提供者获得。

std::futures有两个重要的限制:

  • 每个共享状态只能有一个有效的future,就像只能有一个提供者一样。也就是说,每个提供者只允许创建一个future,并且future永远不能被复制,只能被移动(future也不能被交换)。
  • get()只能调用一次;也就是说,调用get()释放了future对共享状态的引用,使得future无效。在这之后再次调用get()抛出一个异常。本节末尾总结了出现的异常以及出现的时间。

一个shared_future完全等价于一个future,但没有这两个限制:即它们可以被复制,get()可能被调用不止一次。一个shared_future是通过在一个future上调用share()获得的。这也只能做一次,因为它使future无效。但是一旦你有了一个shared_future,更多的可以通过复制来创造。以下是一个概述:

A417649_1_En_7_Figb_HTML.jpg

提供者

<future>库提供了三个不同的提供者:std::async()packaged_taskpromise s。本节将依次讨论每一个。作为异步计算的工作负载示例,我们使用以下最大公约数函数:

A417649_1_En_7_Figc_HTML.gif

异步ˌ非同步(asynchronous)

在返回可用于检索结果的std::future对象之前,调用std::async()调度给定函数的异步执行:

A417649_1_En_7_Figd_HTML.gif

std::thread构造函数一样,几乎可以使用任何类型的函数或函数对象,函数及其参数都被移动或复制到它们的异步执行上下文中。

一旦函数执行完毕,函数调用的结果就进入共享状态。如果函数抛出一个异常,这个异常被捕获并被放入共享状态;如果成功,返回值将被移动到那里。

该标准定义了额外的对std::async()的覆盖,将std::launch的实例作为第一个参数。支持的值至少包括以下enum值(允许实现定义更多):

  • 使用std::launch::async,函数就像在一个新的执行线程中一样被执行,尽管实现可能使用例如线程池来提高性能。
  • 对于std::launch::deferred,直到对async()的这次调用的返回对象之一调用get()时,该函数才被执行。该函数在调用get()的第一个线程中执行。

这些选项可以使用|操作符进行组合。例如,组合async | deferred鼓励实现利用任何可用的并发性,但是如果没有足够的并发性,允许推迟到调用get()时。这种组合也是在没有指定显式启动策略时使用的默认策略。

当使用包含async的启动策略时,有一个重要的注意事项(也就是说,使用默认策略)。从概念上讲,执行异步函数的线程归共享状态所有,共享状态的析构函数与之相联。结果,下面变成了f()的同步执行:

A417649_1_En_7_Fige_HTML.gif

这是因为async()返回的临时future的销毁会一直阻塞到f()执行完毕(内部共享状态的销毁与f()运行的thread汇合)。

Tip

要启动一个函数而不等待它的结果,也就是所谓的一劳永逸,创建一个std::thread对象并detach()它。

打包的任务

一个packaged_task是一个函子,当它的operator()被调用时执行一个给定的函数,然后将结果(即一个值或一个异常)存储到一个共享状态中。例如,这可以用来获取由std::thread执行的函数的结果(回想一下,thread函数的返回值被忽略,如果函数抛出异常,则调用std::terminate()):

A417649_1_En_7_Figf_HTML.gif

用任何函数、仿函数或 lambda 表达式构造的packaged_task有一个相关的共享状态,因此称为valid();默认构造的任务不是valid()。使用get_future()可以获得单个futureget()函数的结果。

像所有的提供者一样,packaged_task不能被复制,只能被移动或交换。这就是为什么在前面的例子中,我们必须将任务仿函数移动到thread(在首先获得它的future之后)。然而,它是唯一可以被多次使用的提供者:有效的packaged_task上的reset()释放其旧的共享状态,并将其与新创建的状态相关联。重置无效任务会引发异常。

有一个额外的成员函数make_ready_at_thread_exit(),它像operator()一样执行任务的函数,除了它直到调用线程退出时才使共享状态就绪。这是在销毁所有线程本地对象之后完成的,用于避免争用情况:

A417649_1_En_7_Figg_HTML.gif

承诺

promisefuture相似,但代表通信通道的输入端,而不是输出端。未来具有阻塞get()功能,承诺提供非阻塞set_value()set_exception()功能。

新的promise是默认构造的,不能复制,只能移动或交换。从每个promise中,可以使用get_future()获得一个单独的future。如果请求第二个,则会引发异常。这里有一个例子:

A417649_1_En_7_Figh_HTML.gif

还有第二组成员函数来填充结果:set_value_at_thread_exit()set_exception_at_thread_exit()。这再次推迟了共享状态的准备,直到调用线程退出,从而确保这发生在销毁任何线程本地对象之后。

例外

如果被误用,<future>头中的大多数函数都会抛出异常。因为所有提供者和返回对象的行为是一致的,所以这一节提供了概述。以下讨论涉及标准异常类以及错误代码和类别的概念,所有这些都将在第八章中详细解释。

像往常一样,默认和移动构造函数、移动赋值操作符和swap()函数被声明为noexcept,当然析构函数也从不抛出异常。除了这些,只有valid()功能是noexcept

provider 和 return 对象的大多数其他成员函数在出错时抛出一个std::future_error,它是std::logic_error的子类。不过,与std::system_error更相似的是,future_error也有一个返回std::error_codecode()成员,在本例中,这个成员的category()等于std::future_category()(其name()等于"future")。对于future_error s,error_codevalue()始终等于错误代码enumstd::future_的四个值之一errc:

  • broken_promise,如果get()在共享状态的返回对象上被调用,而该对象是由共享状态的提供者释放的——因为它的析构函数、移动赋值函数或reset()函数被调用——而没有首先使共享状态就绪。
  • future_already_retrieved,如果get_future()在同一个提供者上被调用两次(没有packaged_taskreset())。
  • promise_already_satisfied,如果通过set功能或通过重新执行packaged_task多次使共享状态就绪。
  • no_state,如果在没有关联状态的提供程序上调用除前面列出的非抛出成员之外的任何成员。对于非valid()返回对象,我们鼓励实现也这样做。

当使用async启动策略时,async()可能会抛出一个带有错误代码resource_unavailable_try_againsystem_error,如果它无法创建一个新线程的话。

互斥<mutex>

互斥(互斥的缩写)是同步对象,用于防止或限制对共享内存和其他资源(如外围设备、网络连接和文件)的并发访问。

除了大量的互斥和锁类型选择之外,<mutex>头还定义了std::call_once(),用于确保给定的函数只被调用一次。本节末尾介绍了call_once()实用程序。

互斥和锁

std::mutex对象m的基本用法如下:

A417649_1_En_7_Figi_HTML.gif

lock()函数会一直阻塞,直到线程获得互斥体的所有权。对于一个基本的std::mutex对象,在任何给定时间只有一个线程被授予独占所有权。目的是只有拥有给定互斥体的线程才被允许访问它所保护的资源,从而防止数据竞争。一个线程保留这个所有权,直到它通过调用unlock()来释放它。一旦解锁,另一个被阻塞在mutex上的线程(如果有的话)被唤醒并被授予所有权。线程被唤醒的顺序是未定义的。

至关重要的是,任何和所有对锁定函数的成功调用都与对unlock()的调用成对出现。为了确保这是以一致和异常安全的方式完成的,您应该避免直接调用这些锁定和解锁函数,而是使用资源获取是初始化(RAII)习惯用法。为此,标准库提供了几个锁类。最简单、最精简的锁是lock_guard,它简单地在其构造函数中调用lock(),在其析构函数中调用unlock():

A417649_1_En_7_Figj_HTML.gif

例子

A417649_1_En_7_Figk_HTML.gif

结果 2000。移除lock_guard几乎肯定会导致小于 2000 的值,当然,除非您的系统不能并发执行线程。

互斥类型

标准库提供了几种风格的互斥体,每一种都比基本的std::mutex有更多的功能。更受限制的互斥类型通常可以更有效地实现。

| 互斥类型 | 递归的 | 超时设定 | 共享 | 页眉 | | --- | --- | --- | --- | --- | | `mutex` | 不 | 不 | 不 | `` | | `recursive_mutex` | 是 | 不 | 不 | `` | | `timed_mutex` | 不 | 是 | 不 | `` | | `recursive_timed_mutex` | 是 | 是 | 不 | `` | | `shared_timed_mutex` | 不 | 是 | 是 | `` | | `shared_mutex`1 | 不 | 不 | 是 | `` |
通用功能

除了前面解释的lock()unlock()函数之外,所有的互斥类型还支持try_lock(),一个lock()的非阻塞版本。如果可以立即获得所有权,则返回true;否则返回false2

实现还可能提供一个native_handle()成员,返回底层本机对象的句柄。

没有一种互斥类型允许复制、移动或交换。

递归

递归互斥体(也称为可重入互斥体)允许已经拥有互斥体的线程调用锁函数。这样做时,锁定会立即成功。但是要小心:为了释放所有权,每次成功调用一个锁函数,都必须调用一次unlock()。因此,和往常一样,最好使用 RAII 锁对象。

对于非递归互斥类型,按照标准,锁定已经拥有的互斥体的行为是未定义的,但是这很可能导致死锁。

超时设定

定时互斥增加了两个额外的锁功能,它们会一直阻塞到给定的超时:try_lock_for(duration)try_lock_until(time_point)。通常,使用<chrono>中定义的类型指定超时,在第二章中解释。两个函数都返回一个布尔值:true如果互斥体的所有权获得成功,或者false如果指定的超时首先发生。

共享所有权<shared_mutex>

只要不被修改,许多类型的共享资源可以被安全地并发访问。例如,对于共享内存,多个线程可以安全地从一个给定的位置读取数据,只要没有线程同时向它写入数据。在这种情况下,限制对单个线程的读访问过于保守,可能会损害性能。

因此,<shared_mutex>头定义了支持共享锁定的互斥体,在它们与所有其他互斥体类型共有的独占锁定模式之上。这种互斥体通常也称为读者-作者互斥体或多读者/单作者互斥体。

想要修改/写入资源的线程必须获得互斥体的独占所有权。这是通过使用与所有互斥类型完全相同的一组函数或锁对象来完成的。然而,只想检查/读取资源的线程可以获得共享所有权。获得共享所有权的成员完全类似于获得独占所有权的成员,除了他们的名称中的lock被替换为lock_shared;也就是说,它们被命名为lock_shared()try_lock_shared_for()等等。使用unlock_shared()释放共享所有权。

当一个或多个线程获得共享所有权时,不授予独占所有权,反之亦然。该标准没有定义授予所有权的顺序,也没有定义线程以任何方式被解除阻塞的顺序。

该标准定义的共享锁目前不支持在没有首先解锁的情况下将所有权从共享升级到独占,或者从独占降级到共享。

锁类型

该标准提供了三种锁类型:std::lock_guard、unique_lock 和 shared_lock。

标准::锁定 _ 保护

lock_guard是一个简单的教科书式的 RAII 模板类:默认情况下,它在构造函数中锁定一个互斥体,在析构函数中解锁。唯一的额外成员是一个构造函数,用于调用线程已经拥有的互斥体。这个构造函数通过传递全局std::adopt_lock常量来调用:

std::lock_guard<std::mutex> lock(m, std::adopt_lock);

标准::唯一 _ 锁定

虽然lock_guard很简单,效率也很高,但是它的功能有限。为了促进更高级的场景,标准定义了unique_lock

基本用法是一样的:

std::unique_lock<std::mutex> lock(m);

然而,unique_locklock_guard相比有几个额外的特性,包括:

  • 一个unique_lock可以移动和交换(当然不能复制)。
  • 它有一个release()函数来解除它与底层互斥体的关联,而不用解锁它。
  • 成员返回一个指向底层互斥体的指针。

然而,真正使unique_lock与众不同的是,它提供了释放和(重新)获得互斥体所有权的功能。具体来说,它支持与底层互斥类型完全相同的一组锁定函数:lock()try_lock()unlock(),以及针对定时互斥类型的定时锁定函数。unique_lock的锁定函数只能被调用一次,即使底层互斥体是递归的,否则将抛出异常。要检查unique_lock是否会在销毁时解锁,调用owns_lock() ( unique_lock也会将这个值转换为布尔值)。

除了带有给定互斥体的显而易见的构造函数之外,unique_lock类还支持三种可选的构造函数,在这些构造函数中传递一个额外的常量:

  • adopt_lock:当互斥体已经被当前线程拥有时使用(类似于等价的lock_guard构造函数)。
  • defer_lock:施工中不要上锁的信号;其中一个锁定功能可以在以后用来锁定互斥体。
  • 尝试在构建期间锁定,但如果失败,则不进行锁定。owns_lock()可用于检查是否成功。
std::shared_lock <shared_mutex>

lock_guardunique_lock都管理互斥体的独占所有权。为了可靠地管理共享所有权,<shared_mutex>定义了std::shared_lock,除了获取/释放共享所有权之外,它完全等同于unique_lock。即使他们获得共享所有权,其锁定和解锁成员的名称也不包含shared。这样做是为了确保shared_lock满足其他实用程序的要求,如std::lock()std::condition_variable_any,两者将在后面讨论。

锁定多个互斥体

一旦线程需要同时获得多个互斥体的所有权,死锁的风险就迫在眉睫。可以采用不同的技术来防止这种死锁:例如,以相同的顺序锁定所有线程中的互斥锁(容易出错),或者所谓的尝试后退方案。标准库提供了模板化的助手函数来促进这一点:

std::lock(lockable1, lockable2, ..., lockableN);

该函数将一直阻塞,直到获得传递给它的所有可锁定对象的所有权。这些可以是互斥体(在锁定后,您应该使用它们的adopt_lock构造函数将其转移到 RAII 锁),但也可以是unique_shared_lock(例如,用defer_lock构造)。尽管标准没有规定如何实现这一点,但是如果所有线程都使用std::lock(),就不会出现死锁。

当然,也存在相当于std::lock()的非阻塞std::try_lock()。它按照对象被传递的顺序对所有对象调用try_lock(),并返回失败的第一个try_lock()的从 0 开始的索引,如果它们都成功,则返回-1。如果它未能锁定对象,任何已锁定的对象将首先被再次解锁。

例外

在互斥体被完全构造之前或被析构之后使用它会导致未定义的行为。如果使用得当,只有下面提到的函数可能会抛出异常。

对于互斥体,所有的lock()lock_shared()函数(不是try_的变体)可能会抛出一个system_error,其中包含一个错误代码(参见第八章):

  • operation_not_permitted,如果调用线程权限不足。
  • resource_deadlock_would_occur如果实现检测到死锁将会发生。不过,死锁检测只是可选的:千万不要依赖它!
  • device_or_resource_busy如果因为底层句柄已经锁定而无法锁定。当然,只针对非递归互斥体,但同样:检测只是可选的。

任何超时的锁定函数,包括try_变量,也可能抛出超时相关的异常。

通过扩展,std::lock()和 RAII 锁的构造函数和锁定函数也可能抛出相同的异常。如果owns_lock() == true(即使底层互斥体是递归的),任何 RAII 锁定函数(包括try_变体)肯定会抛出一个带有resource_deadlock_would_occursystem_error,如果owns_lock() == false,它们的unlock()成员将抛出一个带有operation_not_permitted的。

如果任何锁定函数抛出异常,就可以保证没有互斥锁被锁定。

调用一次函数<mutex>

std::call_once()是一个线程安全的实用函数,确保其他函数最多被调用一次。例如,这对于实现惰性初始化习惯用法很有用:

std::once_flag flag;
...
std::call_once(flag, initialise, "a string argument");

只有用给定的std::once_flag实例调用call_once()的单个线程——一个默认可构造的、不可复制的、不可移动的助手类——有效地执行与其一起传递的函数。任何后续调用都没有效果。如果多个线程同时用同一个标志调用call_once(),那么除了一个之外,所有线程都被挂起,直到执行该函数的线程完成调用。用相同的标志递归调用call_once()会导致未定义的行为。

函数的任何返回值都会被忽略。如果运行函数抛出异常,这将在调用线程中抛出,并且允许另一个线程使用该标志再次执行。如果有线程被阻塞,其中一个会被唤醒。

请注意,call_once()通常比容易出错的双重检查锁定(反)模式更有效,应该始终优先使用。

Tip

函数局部静态(又名魔术静态)与call_once()有着完全相同的语义,但实现起来可能更加高效。因此,尽管call_once()可以很容易地用于单例设计模式的线程安全实现(留给您作为练习),但建议使用函数局部静态:

Singleton& GetInstance() {
   static Singleton instance;
   return instance;
}

条件变量<condition_variable>

条件变量是一个同步原语,它允许线程等待,直到某个用户指定的条件变为true。条件变量总是与互斥体协同工作。这个互斥体还旨在防止检查和设置条件之间的竞争,这本来是由不同的线程完成的。

等待一个条件

假设以下变量在线程间以某种方式共享:

std::mutex m;
std::condition_variable cv;
bool ready = false;

那么等待ready变成true的典型模式是

A417649_1_En_7_Figl_HTML.gif

要使用condition_variable等待,线程必须首先使用std::unique_lock<std::mutex>锁定相应的互斥体。 3wait()阻塞线程时,它也解锁互斥体:这允许其他线程锁定互斥体以满足共享条件。当一个等待线程被唤醒时,在从wait()返回之前,它总是首先使用unique_lock再次锁定互斥体,使得重新检查条件变得安全。

Caution

虽然等待条件变量的线程通常保持阻塞状态,直到对该变量发出通知(稍后讨论),但是它们也有可能(尽管不太可能)在没有通知的情况下随时自动醒来。这些被称为虚假唤醒。这种现象使得像示例中那样始终检查循环中的条件变得至关重要。

或者,所有等待函数都有一个重载,该重载将谓词函数作为参数:可以使用任何返回可以计算为布尔值的函数或仿函数。例如,示例中的循环相当于

cv.wait(lock, [&]{ return ready; });

有两组额外的等待函数,它们永远不会阻塞超过给定的超时:wait_until(time_point)wait_for(duration)。超时总是使用在<chrono>头中定义的类型来表示。wait_until()和 wait_for()的返回值如下:

  • 没有谓词的函数版本从枚举类std::cv_status返回值:timeoutno_timeout
  • 接受谓词函数的重载返回一个布尔值:true,如果谓词在一个通知、一个虚假的唤醒或超时到达后返回true;否则,他们返回false

通知

提供了两个通知函数:notify_all(),它释放所有等待条件变量的线程,和notify_one(),它只释放一个线程。未指定唤醒多个等待线程的顺序。

通知通常发生在条件发生变化时:

{  std::lock_guard<std::mutex> lock(m);
   ready = true;
}
cv.notify_all();

请注意,在调用通知函数时,通知线程不需要拥有互斥体。事实上,任何未阻塞的线程做的第一件事就是试图锁定互斥体,因此在通知之前释放所有权实际上可能会提高性能。 4

还有一个通知函数,但它是非成员函数,具有以下签名:

void std::notify_all_at_thread_exit(condition_variable& cv,
                                    unique_lock<mutex> lock);

当互斥体已经被调用线程通过给定的unique_lock拥有时,并且当没有线程正在等待使用不同互斥体的条件变量时,它将被调用;否则,行为是未定义的。当被调用时,它在删除所有线程本地对象后,在线程退出时调度以下操作序列:

lock.unlock();
cv.notify_all();

例外

如果可用内存不足,条件变量的构造函数可能抛出一个std::bad_alloc,或者如果由于非内存相关的资源限制而无法创建条件变量,则抛出一个带有resource_unavailable_try_againstd::system_error作为错误代码。

析构线程仍在等待的条件变量会导致未定义的行为。

同步

非正式地说,对于单线程程序,优化实现(编译器、内存缓存和处理器的组合)受假设规则的约束。本质上,在一个结构良好的程序中,只要程序的可观察行为(I/O 操作等)就好像指令是按照编写的那样执行的,就可以随意地对指令进行重新排序、省略、发明等等。

然而,在多线程程序中,这还不够。如果没有适当的同步,并发访问共享资源不可避免地会导致数据和其他竞争,即使每个线程都遵守假设规则。

尽管对内存模型的完整、正式的描述超出了本快速参考的范围,但本章还是对不同构造所施加的同步约束进行了简要的非正式介绍,重点放在编写多线程程序时的实际应用上。我们首先使用互斥体介绍所有基本的同步原理。回忆以下内容:

A417649_1_En_7_Figm_HTML.gif

首先,同步构造引入了对单个执行线程中允许的代码重新排序的约束。例如,锁定和解锁互斥体会注入特殊指令,分别称为获取和释放栅栏。这些指令告诉实现(不仅仅是编译器,还有所有执行代码的硬件!)遵守这些规则:任何代码都不能上移获取栏或下移发布栏。总之,这确保了没有代码在临界区之外执行,临界区在lock()unlock()之间。

第二,栅栏在不同的执行线程之间施加约束。这可以解释为对允许并发线程的指令交错到假想的单个指令序列中的限制。例如,在一个线程中释放互斥体的所有权被认为是与在另一个线程中获取互斥体的所有权同步:本质上,在任何交错中,前者必须发生在后者之前。结合前面解释的线程内约束,这意味着在后一个线程进入其临界段之前,前一个线程的整个临界段被保证完全执行。

对于条件变量,同步属性由相应互斥体上的操作所隐含。

对于std::thread s,以下适用:

  • 当启动一个thread时,它的构造函数注入一个释放栅栏,它与线程函数执行的开始同步。这意味着您可以在启动thread之前写入共享内存(例如,初始化它或传递输入),然后安全地(无需额外的同步)从线程函数中访问它。
  • 相反,thread函数执行的结束与它的join()函数内的获取栅栏同步。这确保了加入线程可以安全地读取由线程函数写入的所有共享数据。

最后,对于<future>头中的构造,通过提供者使共享状态就绪包含一个释放栅栏,它与同一共享状态的返回对象的get()内的获取栅栏同步。因此,调用get()的线程不仅可以安全地读取结果(幸运的是),还可以安全地读取提供者编写的任何其他值。例如,future<void>可以用来等待,直到一个线程完成了对共享内存的异步写入。或者一个future<T*>可以指向由提供者函数创建的整个数据结构。

Note

所有这些可以总结如下:不同步的数据竞争(线程并发访问内存,至少有一次写入)的行为是未定义的。然而,只要您始终使用标准库提供的同步结构,您的程序通常会完全按照预期运行。

原子操作<atomic>

首先也是最重要的是,<atomic>头定义了两种类型的原子变量,其操作是原子的或无数据竞争的特殊变量:std::atomic<T>std::atomic_flag。此外,它提供了一些低级函数来显式地引入栅栏,如本节末尾所解释的。

原子变量

std::atomic<T>类型的变量大多表现得像常规的T变量——感谢明显的构造函数、赋值和强制转换操作符——提供了一组有限的细粒度原子操作,具有特定的内存一致性属性。稍后会有更多的细节,但是首先我们介绍一下atomic<T>的模板专门化。

模板专门化和类型定义

atomic<T>模板至少可以与任何普通的可复制的 5 类型T一起使用,并且为布尔以及所有其他整型和指针类型T*定义了专门化。后两者提供了额外的操作,如下所述。

对于布尔和整数特化,定义了便利的typedef。对于std::atomic<xxx>,这些大多等于std::atomic_xxx。具体来说,对于xxx等于boolcharchar16_tchar32_twchar_tshortintlong或者<cstdint>中定义的任何整数类型都是如此(参见第章 1 )。对于剩余的整数类型,typedef缩写了xxx类型的第一个字:

| `typedef` | `xxx` | `typedef` | `xxx` | | --- | --- | --- | --- | | `std::atomic_schar``std::atomic_uchar``std::atomic_ushort` | `signed char``unsigned char``unsigned short` | `std::atomic_ulong``std::atomic_llong` | `unsigned long``long long` |
通用原子操作

一个atomic<T>变量的默认构造函数的行为与一个常规T变量的声明完全一样:也就是说,它通常不初始化值;只有静态或线程本地的atomic变量是零初始化的。用给定的T值初始化的构造函数也存在。不过,这种初始化不是原子的:来自另一个线程的并发访问,即使是通过原子操作,也是一种数据竞争。原子变量不能被复制、移动或交换。

所有的atomic<T>类型都有一个接受T值的赋值操作符和一个转换为T的转换操作符,因此可以用作常规的T变量:

A417649_1_En_7_Fign_HTML.gif

与这些操作符相当的是store()load()成员。例如,前面代码片段的最后两行也可以写成

A417649_1_En_7_Figo_HTML.gif

无论哪种方式,这些操作都是原子的,换句话说,是无数据竞争的。也就是说,如果一个线程同时将一个值存储到一个原子变量中,而另一个线程正在从该原子变量中加载,那么后者看到的要么是存储之前的旧值,要么是新存储的值,而不是两者之间的值(没有半写值)。或者用技术术语来说,不存在撕裂读数。类似地,当两个线程同时存储一个值时,其中一个值被完全存储;从来没有被撕掉的字迹。对于常规变量,这种情况是数据竞争,因此会导致未定义的行为,包括可能的读写错误。

所有的原子变量还提供一些不太明显的原子操作,exchange()compare_exchange。这些成员函数的行为就好像实现如下:

| `T exchange(T newVal) {``T oldVal = load();``store(newVal);``return oldVal;` | `bool compare_exchange(T& oldVal, T newVal) {` `if (load() == oldVal) {` `store(newVal); return true;` `} else {` `oldVal = load(); return false;` |

当然,这两种操作都是原子性的。也就是说,它们(有条件地)以这样一种方式交换值,即在交换期间没有线程可以并发地存储另一个值或经历一次损坏读取。

没有名为compare_exchange的实际成员。相反,有两种不同的变体:compare_exchange_weak()compare_exchange_strong()。唯一(微妙)的区别是前者被允许虚假地失败:也就是说,即使可以进行有效的交换,也会偶尔返回false。这种“弱”变体可能比“强”变体稍快,但只用于循环中。后者旨在用作独立的陈述。

exchange()compare_exchange操作是实现无锁数据结构的关键构件:不使用阻塞互斥的线程安全数据结构。这是一个高级的话题,最好留给专家们来讨论。不过,一个经典的例子是在单链表的开头添加一个新节点:

A417649_1_En_7_Figp_HTML.gif

本节介绍的所有操作对于任何基本类型T都是原子的。对于布尔、整数和指针等类型,大多数编译器只是生成一些保证原子性的特殊指令(目前大多数 CPU 都支持这一点)。如果是,lock_free()返回true。对于其他类型,原子变量大多依靠类似互斥的结构来实现原子性。对于这样的类型,lock_free()返回false

注意:尽管原子变量确保加载和存储是原子的,但这并不意味着底层对象上的操作是原子的。在下面的例子中,如果另一个线程同时调用person对象上的GetLastName(),那么就会与SetLastName()发生数据竞争:

A417649_1_En_7_Figq_HTML.gif

整数和指针类型的原子操作

某些模板专门化提供了额外的操作符来自动更新变量。选择基于当前硬件通常支持的原子指令(例如,无乘法):

  • 原子积分变量:++--+=-=&=|=^=
  • 原子指针变量:++--+=-=

支持前缀和后缀版本的++--。对于其他操作员,等效的非操作员成员也同样可用:分别为:fetch_add()fetch_sub()fetch_and()fetch_or()fetch_xor()

同步

除了原子性之外,原子变量的一个鲜为人知的属性是它们提供了与互斥或线程相同的同步保证。具体来说,所有写入变量的操作(store()exchange s、fetch_xxx())都包含与从同一变量读取的操作(load()exchange s、fetch_xxx()等)中的获取围栏同步的释放围栏。这使得下面的习惯用法成为可能,在将潜在的复杂对象或数据结构存储到共享原子变量中之前,先对其进行初始化:

A417649_1_En_7_Figr_HTML.gif

任何加载指向新对象的指针的线程(在这个例子中是一个Person)也可以安全地读取它所指向的所有其他内存(例如名称字符串),只要这是在释放栅栏之前完全写入的。

所有原子操作(当然除了操作符)都接受一个额外的可选参数(或多个参数),允许调用者微调内存顺序约束。可能的值有memory_order_relaxed, memory_order_consumememory_order_acquirememory_order_release, memory_order_acq_relmemory_order_seq_cst(默认)。例如,第一个选项memory_order_relaxed表示操作必须是原子的,并且不需要进一步的内存顺序约束。其他选项之间的细微差别超出了本书的范围。除非您是专家,否则我们建议您始终坚持使用默认值。否则,您可能会引入微妙的错误。

原子标志

std::atomic_ flag是一个简单的、保证无锁的、原子的、类似布尔的类型。它只能是默认构造的,不能复制、移动或交换。没有指定默认构造函数是否初始化该标志。唯一保证有效的初始化就是这个表达式:

A417649_1_En_7_Figs_HTML.gif

一个atomic_flag只提供另外两个成员:

  • void clear():自动将标志设置为false
  • bool test_and_set():自动将标志设置为true,同时返回其先前值

这两个函数都具有类似于atomic_bool s 的同步属性,并且同样接受可选的std::memory_order参数。

非成员函数

为了与 C 兼容,<atomic>std::atomic<T>std::atomic_flag : atomic_init()atomic_load()atomic_fetch_add()atomic_flag_test_and_set()等等的所有成员函数定义了非成员对等函数。作为一名 C++ 程序员,通常不需要这些:只需使用类的成员函数。

藩篱

<atomic>头还提供了两个函数来显式创建获取和/或释放围栏:std::atomic_thread_fence()std::atomic_signal_fence()。栅栏的概念在本章前面已经解释过了。两者都采用一个std::memory_order参数来指定期望的栅栏类型:memory_order_release表示释放栅栏,memory_order_acquirememory_order_consume表示获取栅栏,memory_order_acq_relmemory_order_seq_cst表示同时是获取和释放栅栏,后一个选项表示栅栏必须是顺序一致的变体(它们的语义差异不在本书讨论范围之内)。带memory_order_relaxed的栅栏没有效果。

这两个函数的区别在于,后者只限制线程和在同一线程中执行的信号处理程序之间的重新排序。后者只约束编译器,但不注入任何指令来约束硬件(内存缓存和 CPU)。

Caution

不鼓励使用显式栅栏:原子变量或其他同步结构具有更有趣的同步属性,通常应该优先使用。

Footnotes 1

预定由标准库的 C++17 版本添加。

  2

虽然通常不常见,但是允许try_lock()虚假地失败:也就是说,即使互斥体不属于任何其他线程,也返回false。在设计更高级的同步场景时要考虑到这一点。

  3

对于condition_variable,必须使用这种确切的锁和互斥类型。为了使用其他标准类型,或者任何具有公共lock()unlock()功能的对象,声明更通用的std::condition_variable_any类,这在其他方面类似于condition_variable

  4

必须注意:它在设置条件和通知等待线程之间引入了一个竞争条件窗口。在某些情况下,在持有锁的同时进行通知实际上可能会导致更可预测的结果,并避免微妙的竞争。如果有疑问,最好不要在通知时解锁互斥体,因为对性能的影响可能很小。

  5

普通的可复制类型没有普通的复制/移动构造函数/赋值,没有虚函数或虚基,也没有普通的析构函数。本质上,这些是可以安全地逐位复制的类型(例如,使用memcpy())。

八、诊断

断言<cassert>

断言是布尔表达式,在代码中的给定点应该是true<cassert>assert宏定义如下:

#ifdef NDEBUG
 #define assert(_)
#else
 #define assert(CONDITION) if (!CONDITION) { print_msg(...); std::abort(); }
#endif

如果断言失败,诊断消息将被写入标准错误输出,并调用std::abort(),这将终止应用程序而不执行任何清理。在调试应用程序时,如果断言失败,某些 ide 会让您选择继续执行。通常的做法是使用断言作为调试辅助,并在构建应用程序的发布版本时定义NDEBUG,将assert变成无操作。

断言通常用于检查不变量,比如循环不变量,或者函数前置和后置条件。一个例子是参数验证:

A417649_1_En_8_Figa_HTML.gif

该程序的一个可能输出是

Assertion failed: msg != nullptr, file d:\Test\Test.cpp, line 13

Caution

确保您提供给assert()的条件没有任何副作用,而这些副作用是正确执行您的程序所必需的,因为如果定义了NDEBUG(例如,对于一个发布版本),这个表达式就不会被计算。

异常,

<exception>中定义的std::exception,它本身并不打算被抛出,而是作为标准库定义的所有异常的基类,并且可以作为你自己的基类。图 8-1 概述了所有标准例外情况。

A417649_1_En_8_Fig1_HTML.jpg

图 8-1。

The C++ Standard Library exception hierarchy

一个exception可以被复制,并提供一个what()方法来返回错误的字符串表示。此函数是虚拟的,应该被重写。返回类型是const char*,但是没有指定字符编码(例如,可以使用编码为 UTF-8 的 Unicode 字符串;参见第六章。

<stdexcept>中定义的异常是唯一由应用程序代码抛出的标准异常。通常,logic_error s 代表程序逻辑中可避免的错误,而runtime_error s 是由超出程序范围的不可预测的事件引起的。logic_errorruntime_error和它们的大部分子类(除了system_error s 和future_error,它们需要一个错误代码,这将在后面讨论)必须在构造时传递一个std::stringconst char*指针,之后由what()返回。因此,无需进一步覆盖what()

异常指针<exception>

<exception>头提供了std::exception_ptr,一种未指定的类似指针的类型,用于存储和传输捕获的异常,即使不知道具体的异常类型。一个exception_ptr可以指向任何类型的值,而不仅仅是一个std::exception。它可以指向自定义异常类、整数、字符串等。只要至少有一个exception_ptr仍在引用它,任何指向的值都保持有效(也就是说,引用计数的智能指针可用于实现exception_ptr)。

<exception>中定义了几个函数来处理异常指针:

  • 当从catch()块内部直接或间接调用时,创建并返回一个引用当前正在运行的异常的exception_ptr(记住,这可以是任何类型的异常)(catch()块可以调用一个助手函数来处理异常)。如果在没有异常被处理时调用,返回的exception_ptr指的是空值。
exception_ptr std::current_exception() noexcept

  • 创建并返回一个指向texception_ptr
template<typename T>
exception_ptr std::make_exception_ptr(T t) noexcept

  • 重新抛出给定的exception_ptr指向的异常。这是获得由一个exception_ptr指向的对象的唯一方法。一个exception_ptr不能被解引用,也没有 getter 函数。
[[noreturn]] void std::rethrow_exception(exception_ptr)

一旦被创建,exception_ptr可以被复制、比较,特别是与nullptr进行赋值和比较。这使得它们在存储和移动异常以及稍后测试异常是否发生时非常有用。为此,exception_ptr也可以转换为布尔值:true如果它指向一个异常,false如果它是一个空指针。默认构造的实例相当于nullptr

例如,异常指针可用于将异常从工作线程转移到主线程(注意,这也是上一章讨论的<future>实用程序隐式为您做的事情):

A417649_1_En_8_Figb_HTML.gif

嵌套异常<exception>

<exception>头文件还提供了处理嵌套异常的工具。它们允许您将捕获的异常封装在另一个异常中:例如,用额外的上下文信息扩充它,或者将其转换为更适合您的应用程序的异常。std::nested_exception是一个可复制的 mixin 1 类,其默认构造函数捕获current_exception()并存储。这个嵌套的异常可以作为一个带有nested_ptr()exception_ptr来检索,或者通过使用rethrow_nested()来重新抛出它。但是要小心:当调用rethrow_nested()而没有存储任何异常时,会调用std::terminate()。因此,通常建议您不要直接使用nested_exception,而是使用这些辅助方法:

  • 抛出一个从std::nested_exceptionT派生的未定义类型(去掉了引用限定符),可以使用常规的catch (const T&)表达式处理,忽略嵌套的异常。作为一个std::nested_exception,它也包含了std::current_exception()的结果,可以随意地检索和处理。
[[noreturn]] template<typename T> void std::throw_with_nested(T&& t)

  • 如果t是从nested_exception派生出来的,就在上面调用rethrow_nested();否则什么也不做。
template <typename T> void std::rethrow_if_nested(const T& t)

下面的示例演示了嵌套异常:

void execute_helper() {
   throw std::range_error("Out-of-range error in execute_helper()");
}
void execute() {
   try { execute_helper(); }
   catch (...) {
      std::throw_with_nested(std::runtime_error("Caught in execute()"));
   }
}
void print(const std::exception& exc) {
   std::cout << "Exception: " << exc.what() << std::endl;
   try { std::rethrow_if_nested(exc); }
   catch (const std::exception& e) {
      std::cout << "   Nested ";
      print(e);
   }
}
int main() {
   try { execute(); }
   catch (const std::exception& e) { print(e); }
}

这段代码的输出如下:

Exception: Caught in execute()
   Nested Exception: Out-of-range error in execute_helper()  

系统错误<system_error>

来自操作系统或其他低级 API 的错误称为系统错误。这些由在<system_error>头中定义的类和函数处理:

  • error_code: Generally wraps a platform-specific error code (an int), although for some categories the error codes are defined by the standard (see Table 8-1).

    表 8-1。

    Available Error Category Functions and Corresponding Error Condition and Error Code Enum Classes

    | 单一函数 | 错误条件 | 错误代码 | 页眉 | | --- | --- | --- | --- | | `generic_category()` | `std::errc` |   | `` | | `system_category()` |   |   | `` | | `iostream_category()` |   | `std::io_errc` | `` | | `future_category()` |   | `std::future_errc` | `` |
  • error_condition: Wraps a portable, platform-independent error condition (an int). The enum class std::errc lists the built-in conditions. They correspond to the standard POSIX error codes, defined also as macros in <cerrno>. See Table 8-2 at the end of this chapter.

    表 8-2。

    std::errc Error Condition Values and Corresponding <cerrno> Macros

    | `std::errc enum`值 | ``宏 | | --- | --- | | `address_family_not_supported` | `EAFNOSUPPORT` | | `address_in_use` | `EADDRINUSE` | | `address_not_available` | `EADDRNOTAVAIL` | | `already_connected` | `EISCONN` | | `argument_list_too_long` | `E2BIG` | | `argument_out_of_domain` | `EDOM` | | `bad_address` | `EFAULT` | | `bad_file_descriptor` | `EBADF` | | `bad_message` | `EBADMSG` | | `broken_pipe` | `EPIPE` | | `connection_aborted` | `ECONNABORTED` | | `connection_already_in_progress` | `EALREADY` | | `connection_refused` | `ECONNREFUSED` | | `connection_reset` | `ECONNRESET` | | `cross_device_link` | `EXDEV` | | `destination_address_required` | `EDESTADDRREQ` | | `device_or_resource_busy` | `EBUSY` | | `directory_not_empty` | `ENOTEMPTY` | | `executable_format_error` | `ENOEXEC` | | `file_exists` | `EEXIST` | | `file_too_large` | `EFBIG` | | `filename_too_long` | `ENAMETOOLONG` | | `function_not_supported` | `ENOSYS` | | `host_unreachable` | `EHOSTUNREACH` | | `identifier_removed` | `EIDRM` | | `illegal_byte_sequence` | `EILSEQ` | | `inappropriate_io_control_operation` | `ENOTTY` | | `interrupted` | `EINTR` | | `invalid_argument` | `EINVAL` | | `invalid_seek` | `ESPIPE` | | `io_error` | `EIO` | | `is_a_directory` | `EISDIR` | | `message_size` | `EMSGSIZE` | | `network_down` | `ENETDOWN` | | `network_reset` | `ENETRESET` | | `network_unreachable` | `ENETUNREACH` | | `no_buffer_space` | `ENOBUFS` | | `no_child_process` | `ECHILD` | | `no_link` | `ENOLINK` | | `no_lock_available` | `ENOLOCK` | | `no_message` | `ENOMSG` | | `no_message_available` | `ENODATA` | | `no_protocol_option` | `ENOPROTOOPT` | | `no_space_on_device` | `ENOSPC` | | `no_stream_resources` | `ENOSR` | | `no_such_device` | `ENODEV` | | `no_such_device_or_address` | `ENXIO` | | `no_such_file_or_directory` | `ENOENT` | | `no_such_process` | `ESRCH` | | `not_a_directory` | `ENOTDIR` | | `not_a_socket` | `ENOTSOCK` | | `not_a_stream` | `ENOSTR` | | `not_connected` | `ENOTCONN` | | `not_enough_memory` | `ENOMEM` | | `not_supported` | `ENOTSUP` | | `operation_canceled` | `ECANCELED` | | `operation_in_progress` | `EINPROGRESS` | | `operation_not_permitted` | `EPERM` | | `operation_not_supported` | `EOPNOTSUPP` | | `operation_would_block` | `EWOULDBLOCK` | | `owner_dead` | `EOWNERDEAD` | | `permission_denied` | `EACCES` | | `protocol_error` | `EPROTO` | | `protocol_not_supported` | `EPROTONOSUPPORT` | | `read_only_file_system` | `EROFS` | | `resource_deadlock_would_occur` | `EDEADLK` | | `resource_unavailable_try_again` | `EAGAIN` | | `result_out_of_range` | `ERANGE` | | `state_not_recoverable` | `ENOTRECOVERABLE` | | `stream_timeout` | `ETIME` | | `text_file_busy` | `ETXTBSY` | | `timed_out` | `ETIMEDOUT` | | `too_many_files_open` | `EMFILE` | | `too_many_files_open_in_system` | `ENFILE` | | `too_many_links` | `EMLINK` | | `too_many_symbolic_link_levels` | `ELOOP` | | `value_too_large` | `EOVERFLOW` | | `wrong_protocol_type` | `EPROTOTYPE` |
  • error_category:错误代码和情况属于一个类别。类别单例对象负责两种数字之间的转换。

  • system_error:一个异常类(见图 8-1 ),有一个额外的code()成员返回一个error_code

除了一个数值之外,error_codeerror_condition对象都有一个对它们的error_category的引用。在一个类别中,一个编号是唯一的,但是同一编号可能被不同的类别使用。

所有这些看起来相当复杂,但是这些错误的主要用途仍然很简单。为了比较一个给定的错误代码,比如来自一个被捕获的system_error异常的错误代码,可以使用==!=操作符。例如:

if (systemError.code() == std::errc::argument_out_of_domain)
   ...

Note

std::ios_base::failure(第章 5 )和future_error(第章 7 )工作类似。它们还有一个code()成员返回一个error_code,可以使用==!=与已知的代码值(见表 8-1 )进行比较。

标准::错误 _ 类别

不同的std::error_category实例被实现为单例:也就是说,每个类别只有一个全局的、不可复制的实例。存在许多预定义的类别,可从表 8-1 中列出的全局函数中获得。

一个std::error_category有以下方法:

| 成员 | 描述 | | --- | --- | | `name()` | 返回类别的名称(作为一个`const char*`)。 | | `message()` | 返回给定错误条件值的解释性`std::string`(一个`int`)。 | | `default_error_condition()` | 将给定的错误代码值(an `int`)转换为可移植的`error_condition`。 | | `equivalent()` | 将错误代码与便携条件进行比较。相反,使用前面显示的`==`和`!=`操作符更容易。 |

标准::错误代码

std::error_code封装一个错误码值和一个error_category。有三个构造函数:

  • 将错误代码设置为0(这通常表示“无错误”)并将其与system_category相关联的默认设置。
  • 一个接受错误代码int和一个error_category
  • 一个是通过调用std::make_error_code(e)从错误代码枚举值e构造一个error_code。参数类型必须是错误代码枚举类型,即std::is_error_code_enum类型特征的值为true的枚举类型(类型特征参见第二章)。这也会自动设置正确的类别。标准类别的枚举类如表 8-1 所示。

要提升你自己的std::system_error,你必须提供一个error_code,它可以用它的一个构造函数或者用make_error_code()来创建。例如:

A417649_1_En_8_Figc_HTML.gif

std::error_code提供了以下方法:

| 方法 | 描述 | | --- | --- | | `assign(int,` `error_category&)` | 将给定的错误代码和类别分配给此`error_code` | | `operator=` | 使用`std::make_error_code()`给这个`error_code`分配一个给定的错误代码枚举值 | | `clear()` | 将错误代码设置为 0,并将类别设置为`system_category`以表示没有错误 | | `int value()` `error_category& category()` | 返回错误值/相关类别 | | `error_condition` `default_error_condition()` | 调用`category().default_error_condition(value())`,返回相应的便携错误条件 | | `string message()` | 通话次数`category().message(value())` | | `operator bool` | 如果错误代码不为 0,则返回`true` |

标准::错误条件

std::error_condition类封装了一个可移植的条件代码和相关的错误类别。这个类有一组类似于error_code的构造函数和方法,除了

  • 它没有从错误状态到错误代码的default_error_condition()方法或等效函数。
  • 使用错误条件枚举来代替错误代码枚举:这些枚举类型的is_error_condition_enum类型特征的值为true
  • 使用std::make_error_code()的成员使用std::make_error_condition()代替。

c 错误号<cerrno>

<cerrno>头定义了errno,一个扩展到与int&相等的值的宏。函数可以将errno的值设置为特定的错误值,以发出错误信号。每个执行线程都有一个单独的errno。设置errno对于 C 头文件中的函数来说非常常见。C++ 库大多在失败时抛出异常,尽管有些库也设置了errno(例如std::string-数字转换)。表 8-2 列出了由<cerrno>定义的带有默认 POSIX 错误号的宏。

如果您想使用errno来检测使用errno来报告错误的函数中的错误,那么您必须确保在调用该函数之前将errno设置为0,就像本例中所做的那样(需要<cmath> ) 2 :

A417649_1_En_8_Figd_HTML.gif

输出取决于您的平台,但可能如下所示:

Error: result out of range

为了完整起见,我们展示了两种报告当前errno的错误字符串的替代方法。它们分别使用来自<cstring>strerror()(注意:这个函数不是线程安全的!)和来自<cstdio>std::perror()。下面两行打印了一条类似于前面代码的消息:

A417649_1_En_8_Fige_HTML.gif

故障处理<exception>

STD::un capture _ exception()

如果在代码中的任何地方,您想知道当前是否有一个尚未被捕获的异常正在进行中——换句话说,检测堆栈展开正在进行中——使用uncaught_exception(),如果是这样,它将返回true

Note

通常没有理由或安全的方法来使用uncaught_exception(),所以我们建议不要使用它。这里提到它只是为了完整。

std::terminate()

如果异常处理由于某种原因失败了——例如,异常被抛出但从未被捕获——那么运行时调用std::terminate(),它调用终止处理程序。默认处理程序调用std::abort(),这反过来中止应用程序而不执行任何进一步的清理。使用来自<exception>的以下函数管理主动终止处理程序,其中std::terminate_handler是函数指针类型,必须指向不带参数的void函数:

std::terminate_handler std::set_terminate(std::terminate_handler) noexcept
std::terminate_handler std::get_terminate() noexcept

自定义终止处理程序的一个用例是在调用std::terminate()时自动生成一个进程转储。拥有一个转储文件来进行分析极大地帮助了追踪触发流程到terminate()的 bug。您应该考虑为任何专业应用程序设置此功能。

std::意外()

如果动态异常规范 3 被忽略,运行时调用std::unexpected():也就是说,如果一个函数抛出了它不被允许的东西。类似于terminate(),这个函数调用一个std::unexpected_handler函数,可以使用std::set_unexpected() / get_unexpected()来管理这个函数。默认处理程序调用std::terminate()

Note

动态异常规范和std::unexpected()都已被弃用,这里仅是为了完整性而提及。

Footnotes 1

mixin 是一个类,它提供了一些添加到其他类的功能(在这种情况下,存储指向嵌套异常和一些相关函数的指针的能力)。在 C++ 中,mixins 一般通过多重继承来实现。

  2

std::exp()仅对<cmath>中定义的math_errhandling包含MATH_ERRNO的实现设置errno:参见第一章。不过,大多数情况似乎都是如此。

  3

动态异常规范是函数声明的一部分,用逗号分隔的列表指定允许函数抛出哪些异常。比如:ReturnType Func(...) throw(exception1, exception2, ...);