原文详见:C++20: Concurrency
本篇是 C++20 概览系列的最后一篇。今天我将介绍 C++ 新标准中与并发有关的特性。
并发
C++20 在并发方面有诸多改善。
std::atomic_ref<T>
类模板 std::atomic_ref 将原子操作应用于引用的非原子对象。 因此,对引用对象的并发读写是没有数据竞争的。引用对象的生存期必须超过 atomic_ref 的生存期。使用 atomic_ref 访问被引用对象的子对象不是线程安全的。
相应地,std::atomic,std::atomic_ref 可以被特化,并支持对内置数据类型进行特化。
struct Counters {
int a;
int b;
};
Counter counter;
std::atomic_ref<Counters> cnt(counter);
std::atomic<std::shared_ptr<T>> 和 std::atomic<std::weak_ptr<T>>
shared_ptr 是惟一可以应用原子操作的非原子数据类型。首先,让我来说明一下这样设计的动机。 C++ 委员会认为 std::shared_ptr 实例应该为多线程程序提供最小的原子性保证。那么 std::shared_ptr 的最小原子性保证是什么?shared_ptr 的控制块是线程安全的。这意味着增加和减少引用计数器的操作是原子性的。你还可以保证资源只被销毁一次。
std::shared_ptr 提供的断言由 Boost 描述:
- shared_ptr 实例可以被多个线程同时“读取”(仅使用常量操作访问)。
- 不同的 shared_ptr 实例可以被多个线程同时“写入”(通过操作符 = 或 reset 等可变操作访问)(即使这些实例是副本,并且在底层共享相同的引用计数)。
在 C++20 中,我们得到了两个新的智能指针:std::atomic<std::shared_ptr<T>> 和 std::atomic<std::weak_ptr<T>>。
浮点原子量
In addition to C++11, you can not only create atomics for integral types but also for floating points. This is quite convenient when you have a floating-point number, which is concurrently incremented by various threads. With a floating-point atomic, you don't need to protect the floating pointer number.
Waiting on Atomics
std::atomic_flag is an atomic boolean. It has a clear and a set state. For simplicity reasons, I call the clear state false and the set state true. Its clear method enables you to set its value to false. With the test_and_set method, you can set the value to true and return the previous value. There is no method to ask for the current value. This will change with C++20. With C++20, a std::atomic_flag has a test method.
Additionally, std::atomic_flag can be used for thread synchronisation via the methods notify_one, notify_all, and wait. Notifying and waiting is with C++20 available on all partial and full specialisation of std::atomic (bools, integrals, floats and pointers) and std::atomic_ref.
Semaphores, Latches and Barriers
All three types are means to synchronise threads.
信号量
Semaphores are a synchronisation mechanism used to control concurrent access to a shared resource. A counting semaphore such as the on in C++20, is a special semaphore which has a counter that is bigger than zero. The counter is initialised in the constructor. Acquiring the semaphore decreases the counter and releasing the semaphore increases the counter. If a thread tries to acquire the semaphore when the counter is zero, the thread will block until another thread increments the counter by releasing the semaphore.
Latches and Barriers
Latches and barriers are simple thread synchronisation mechanisms which enable some threads to block until a counter becomes zero.
What are the differences between these two mechanisms to synchronise threads? You can use a std::latch only once, but you can use a std::barrier more than once. A std::latch is useful for managing one task by multiple threads; a std::barrier is useful for managing repeated tasks by multiple threads. Additionally, a std::barrier can adjust the counter in each iteration.
The following code snippet is from the Proposal N4204.
latch completion_latch(NTASKS); // (1)
for (int i = 0; i < NTASKS; ++i) {
pool->add_task([&] { // (2)
// perform work
...
completion_latch.count_down();// (4)
})}; // (3)
}
// Block until work is done
completion_latch.wait(); // (5)
}
The std::latch completion_latch is in its constructor set to NTASKS (line 1). The thread pool executes NTASKS (lines 2 - 3) jobs. At the end of each job (line 4), the counter is decremented. Line 5 is the barrier for the thread running the function DoWork and hence for the small workflow. This thread is blocked until all tasks have been finished.
std::jthread
std::jthread stands for joining thread. In addition to std::thread from C++11, std::jthread can automatically join the started thread and can be interrupted.
Here is the non-intuitive behaviour of std::thread. If a std::thread is still joinable, std::terminate is called in its destructor. A thread thr is joinable if either thr.join() nor thr.detach() was called.
// threadJoinable.cpp
#include <iostream>
#include <thread>
int main(){
std::cout << std::endl;
std::cout << std::boolalpha;
std::thread thr{[]{ std::cout << "Joinable std::thread" << std::endl; }};
std::cout << "thr.joinable(): " << thr.joinable() << std::endl;
std::cout << std::endl;
}
When executed, the program terminates.

Both executions of the program terminate. In the second run, the thread thr has enough time to display its message: “Joinable std::thread”.
In the next example, I replace the header <thread> with "jthread.hpp" and, therefore, use std::jthread from the upcoming C++20 standard.
// jthreadJoinable.cpp
#include <iostream>
#include "jthread.hpp"
int main(){
std::cout << std::endl;
std::cout << std::boolalpha;
std::jthread thr{[]{ std::cout << "Joinable std::jthread" << std::endl; }};
std::cout << "thr.joinable(): " << thr.joinable() << std::endl;
std::cout << std::endl;
}
Now, the thread thr automatically joins in its destructor such as in this case if still joinable.

接下来?
我在前四篇文章中概述了 C++20 中的新特性。在概述之后,让我们深入讨论细节。下一篇文章将从概念开始。
网友评论