C++ Concurrency in Action笔记

阅读《C++ Concurrency in Action》时记录下的笔记

std::thread works with any callable type, e.g.. a class instance with operator().

注意避免C++'s most vexing parse,也就是类的无参构造时,应像下面这样写。

如果没有定义std::thread该如何运行,std::thread的destructor会在调用std::terminate,所以应在其析构前决定它是join还是detach。

用RAII保证它exception-safe。

std::thread的参数传递

默认情况下,传给std::thread的内容会被copy或move到std::thread对象内,在callable object被调用时才会被以rvalue形式进一步传给函数。所以当函数的参数为非const的reference时,编译器将报错。

如果想要引用当前的变量,可以使用std::ref。

如果想要在其他线程执行成员函数,可以用如下方式。

如果成员函数有参数,可以跟在后面。

可以用std::thread::hardware_concurrency()查看支持的线程数,但如果该信息不可用则会返回0。

注意创建线程时-1因为还有该线程自身。

与迭代器有关的一系列函数

std::distance 给定两个迭代器之间的元素个数

std::advance 将给定迭代器移动指定距离

std::accumulate 两个迭代器之间的元素的和

Identifying threads

每个underlying的thread都有一个独特的编号,可以用std::thread::get_id()得到thread object的id object,如果该thread object没有underlying thread,那就会返回一个default-destructed的id object。

可以使用std::this_thread::get_id()得到当前thread的underlying thread的id object。

std::thread::id是total order的

Sharing data between threads

通过std::mutex实现。

std::mutex实际上并不是给数据加锁,而是给代码加锁,所有用同一个std::mutex锁住的代码段不能同时执行。

但这并不是说,当我们实现一个类时,只需要将会发生race或invariant break的代码段加锁就不会出现问题。如果将需要保护的数据作为pointer、reference返回出去,或作为参数传递给函数,使用者并没有意识到这是多线程危险的,所以不会加锁,这就会导致问题。

Don’t pass pointers and references to protected data outside the scope of the lock, whether by returning them from a function, storing them in externally visible memory, or passing them as arguments to user-supplied functions.

此外,当一个object在多个线程间被共享时,也可能出现问题。比如stack,其他线程在进行push、pop而有一线程在根据stack内元素个数进行不同的操作,即使使用了std::mutex,也可能会出现判断完了之后条件不成立的情况。

Deadlock

上图代码中,如果第一个函数的第一行被一个线程执行,然后第二个函数的第一行被另一个执行,就出现了死锁。

deadlock造成的原因为不同函数中上锁顺序可能不同。为避免这点,C++提供了一个一次性上多个锁的机制,即std::lock。对于已经上锁的使用std::lock_guard需要在第二个参数传入std::adopt_lock,因为对已经上mutex锁的调用lock是未定义行为。

此外,可以统一锁的顺序。可以用hierarchy的概念对该顺序进行规定,每个锁在初始化时传入一个hierarchy,hierarchy大的锁必须在hierarchy小的锁前面lock。

C++17的std::scoped_lock提供了更简单的实现,它类似于std::lock_guard,但是可以直接用于多个mutex。

死锁同样还可能在两个或多个thread互相等待彼此join时发生,所以写代码时应该避免这一点。

std::unique_lock可以用于mutex不立刻上锁的场景,通过第二个参数std::defer_lock,之后用std::lock上锁。但由于它需要额外的开销维护锁是否上了,效率并不是很高。owns_lock可以检测是否上锁。通常情况下,如果没有转移owner_ship的要求以及defer的需要,且在C++17以上,用scope_lock是更好的选择。

std::unique_lock支持手动解锁、加锁。

Locking at an approporiate granularity

Unless the lock is intended to protect access to the file, don't do any time-consuming activities like file I/O whilte holding a lock.

Alternative facilities for protecting shared data

Protecting shared data during initialization

C++11以后,static变量的初始化多线程安全。

可以用std::once_flag和std::call_once来确保初始化只进行一次。这样的做法往往比用std::mutex开销小许多。

Protecting rarely updated data structures

C++14提供了std::shared_timed_mutex,C++17提供了std::shared_mutex,std::shared_timed_mutex提供了更多的功能,但是开销会稍有增加。

The performance is dependent on the number of processors involved and the relative workloads of the reader and updater threads. It’s therefore important to profile the performance of the code on the target system to ensure that there’s a benefit to the additional complexity.

std::shared_mutex支持共享锁和互斥锁。std::lock_guard、std::unique_lock和std::mutex,提供exclusive access。C++14提供的std::shared_lock用于加共享锁。

给有共享锁的加互斥锁和给有互斥锁的加共享锁都会阻塞。

Recursive locking

当一个加锁的成员函数调用另一个加锁的成员函数时,可以用std::recursive_mutex代替std::mutex。但是这往往不是一个好的解决方案,因为第二个函数可能会在invariant broken的情况下运行。一个更好的解决方案是写一个新的不加锁的成员函数,并仅在加锁的情况下调用该函数。

Synchronizing concurrent operations

Waiting for an event or other condition

C++提供了std::condition_variable和std::condition_variable_any(more general but less flexibility).

condition_variable提供了wait方法,该方法接受一个std::unique_lock和一个函数。如果函数返回false就解开锁并且将当前thread变成blocked或waiting state,直到这个condition_variable在另一个线程中被notify()。一旦notify,重新上锁并再一次调用函数检测。如果返回为true则保持上锁状态继续执行,否则同上。

使用notify_one从所有等待的线程中选一个响应,notify_all使得所有线程响应。

wait的一种简单实现如下。

Waiting for one-off events with futures

std::future是move-only的,只能进行一次get操作。

用std::async

用std::packaged_task给function或callable object绑定std::future。结合使用它与队列,可以给一个线程分配任务。

用std::thread运行std::packaged_task,就可以通过它对应的std::future得到返回值。


std::promise object可以用get_future方法得到std::future,而std::promise object可以通过set_value、set_exception设置值或异常作为std::future的get的返回内容。一旦设置,std::future就为ready。


std::future中可以存放值或exception,在get时exception被重新抛出,但C++并没有规定重新抛出的exception是否和原本的一致,所以不同编译器实现可能有不同。

如果已经知道要抛出的错误是什么,最好直接用下面的语句,因为它效率更高,更易读。

如果std::promise没有set或std::packaged_task没有被call,那么它们析构的时候就会在关联的future中store a std::future_error exception with an error code of std::future_errc::broken_promise。

std::future只能有一个thread等待它的结果,std::shared_future支持多个。


std::shared_future是copyable的,可以通过future.share()得到,也可以通过move future得到。

Waiting with a time limit

Clock

std::chrono::system_clock:通常而言,这个时钟并不稳定,因为它会有一个自动校准的过程,这甚至可能导致后来的now小于之前的now。

std::chrono::steady_clock:这个时钟非常稳定。

std::chrono::high_resolution_clock:这个时钟的period最小,因而可以提供最高的精度。

some_clock::now()返回类型是some_clock::time_point。

some_clock::period是时钟周期typedef,单位秒,是std::ratio

Durations

std::chrono::duration第一个模板参数表示用来存储该时间的数据类型,第二个参数为std::ratio,用来指定该单位duration的长度。

C++ 在std::chrono namespace里提供了nanoseconds、microseconds、milliseconds、seconds、minutes、hours,它们都用足够大的integral类型表示duration。

C++14引入了std::chrono_literals namespace,支持用后缀h、min、ms等表示duration。

对于整数,上面两种方式是等价的。

对于浮点数,如果关注范围和精度,应使用std::chorono::duration。

当不需要截断时,也即大的单位转小的单位,转换是隐式的。但反过来则需要显式转换。C++提供了std::chrono::duration_cast用于时间单位的转换。该转换是截断而不是取整。

使用wait_for

去掉单位的整数可以用.count()获得


std::future::wait_for方法接受一个duration参数,返回类型为std::future_status,如果是timeout则说明等待时间到了,如果是ready说明future在ready状态,如果是deferred说明future的task在deferred状态。wait_for使用的是steady_clock。

Time points

std::chrono::time_point第一个模板参数为时钟类型,第二个为单位。

使用wait_until

如果只是等待固定的时间,应使用wait_until,因为它不存在虚假唤醒的问题,到达时间后自动唤醒,而wait_for则是隔一段时间唤醒一次检查条件。

std::condition_variable::wait_until返回类型为std::cv_status。

Functions that accept timeouts

std::future、std::condition_variable、std::this_thread::sleep_for、std::this_thread::sleep_until。

std::timed_mutex、std::recursive_timed_mutex、std::shared_timed_mutex支持try_lock_for和try_lock_until

Using synchronization of operations to simplify code

Functional programming with futures

Functional programming指函数和外部状态无关,只和传入的参数有关,且不会修改外部状态,即只要传入参数一致,得到的结果必然相同。这种范式非常容易改成并发。

Synchronizing operations with message passing

CSP(Communicating Sequential Process):没有shared data,每个thread独立运行,只基于它受到的message。实现类似于有限状态机。

Continuation-style concurrency with the Concurrency TS


C++ Concurrency in Action笔记
https://jhex-git.github.io/posts/3350115786/
作者
JointHex
发布于
2022年9月30日
许可协议