一、多线程编程的同步
在传统的多线程(进程)的编程中,处理数据共享是一个重中之重。在目前流行的多核(多CPU)编程中,虽然采用了更多的分布式的算法,但最终细分到一个处理单元中,仍然是处理线程间数据的拆分。换句话说,通过数据结构的设计和算法的分拆,实现最小的数据冲突结果。
说这些目的在于告诉大家,多线程本地的编程,从目前看,仍然是无法回避的,仍然在编程中是一个重要的环节。而解决多线程编程中的一个重要的问题就是如何处理数据的同步问题,每个程序员可能直接就会报出来有mutex,event,condition等等。也有的会提到c++11后的lock等。但是如果有其它一些语言编程的经验的人会发现c++标准库提供的同步数据结构还是要少很多。
当然,网上有些大牛提出过,这些足够了,确实也是如此。可是针对一些不想成长为大牛甚至只想当一个熟练工的程序员来说,封装一些同步的数据结构,对他来说就是一种痛苦,而用别人的封装又怕出了问题不好解决,怎么办?c++20进一步满足了这种应用场景。
二、c++20中的同步库
在c++20中增加了以下几类同步数据结构:
1、信号量(Semaphore)
轻量级的同步原语,可以实现 mutex, latches, barriers, …等同步数据结构。
有两种表现类型:
多元信号量(counting semaphore): 建模非负值资源计数
二元信号量(binary semaphore): 只有两个状态的信号量
主要方法有:
release:
增加内部计数器并对获取者解除阻塞
acquire :
减少内部计数器或阻塞到直至能获取
try_acquire:
尝试减少内部计数器而不阻塞
try_acquire_for :
尝试减少内部计数器,至多阻塞一段时长
try_acquire_until:
尝试减少内部计数器,阻塞直至一个时间点
其实它的用法和以前类似的库的用法都差不多。
2、std::atomic 等待和通知接口
等待/阻塞在原子对象直到其值发生改变, 然后通知函数发送通知,它比单纯的自旋锁和轮询要效率高。
主要方法有:
wait:
阻塞线程直至被提醒且原子值更改
notify_one:
提醒至少一个在原子对象上的等待中阻塞的线程
notify_all:
提醒所有在原子对象上的等待中阻塞的线程
这个其实是实现CAS的,在以前就有,在c++20中又增加了相关的一些具体的实现罢了。
3、锁存器(Latches)
latch 是 std::ptrdiff_t 类型的向下计数器,它能用于同步线程。在创建时初始化计数器的值。线程可能在 latch 上阻塞直至计数器减少到零。没有可能增加或重置计数器,这使得 latch 为单次使用的屏障。同时调用 latch 的成员函数,除了析构函数,不引入数据竞争。
注意:它区别于下面的Barriers的是它只有使用一次。
4、屏障(Barriers)
std::barrier 提供允许至多为期待数量的线程阻塞直至期待数量的线程到达该屏障。不同于 std::latch ,屏障可重用:一旦到达的线程从屏障阶段的同步点除阻,则可重用同一屏障。
三、具体实例
c++20中的具体的例子参看如下:
#include <array>
#include <chrono>
#include <cstddef>
#include <iomanip>
#include <iostream>
#include <mutex>
#include <random>
#include <semaphore>
#include <thread>
#include <vector>
using namespace std::literals;
constexpr std::size_t max_threads{10U}; // 更改并查看效果
constexpr std::ptrdiff_t max_sema_threads{3}; // 对二元信号量为 {1}
std::counting_semaphore semaphore{max_sema_threads};
constexpr auto time_tick{10ms};
unsigned rnd() {
static std::uniform_int_distribution<unsigned> distribution{2U, 9U}; // [延迟]
static std::random_device engine;
static std::mt19937 noise{engine()};
return distribution(noise);
}
class alignas( 128 /*std::hardware_destructive_interference_size*/ ) Guide {
inline static std::mutex cout_mutex;
inline static std::chrono::time_point<std::chrono::high_resolution_clock> started_at;
unsigned delay{rnd()}, occupy{rnd()}, wait_on_sema{};
public:
static void start_time() { started_at = std::chrono::high_resolution_clock::now(); }
void initial_delay() { std::this_thread::sleep_for(delay * time_tick); }
void occupy_sema() {
wait_on_sema =
static_cast<unsigned>(std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::high_resolution_clock::now() - started_at - delay * time_tick)
.count() / time_tick.count());
std::this_thread::sleep_for(occupy * time_tick);
}
void visualize(unsigned id, unsigned x_scale = 2) const {
auto cout_n = [=] (auto str, unsigned n) {
n *= x_scale;
while (n-- > 0) { std::cout << str; }
};
std::lock_guard lk{cout_mutex};
std::cout << "#" << std::setw(2) << id << " ";
cout_n("░", delay);
cout_n("▒", wait_on_sema);
cout_n("█", occupy);
std::cout << '\n';
}
static void show_info() {
std::cout
<< "\nThreads: " << max_threads << ", Throughput: " << max_sema_threads
<< " │ Legend: initial delay ░░ │ wait state ▒▒ │ sema occupation ██ \n"
<< std::endl;
}
};
std::array<Guide, max_threads> guides;
void workerThread(unsigned id) {
guides[id].initial_delay(); // 模拟获取旗标前的某些工作
semaphore.acquire(); // 等待直至空闲旗标槽可用
guides[id].occupy_sema(); // 模拟获取旗标时的某些工作
semaphore.release();
guides[id].visualize(id);
}
int main() {
std::vector<std::jthread> threads;
threads.reserve(max_threads);
Guide::show_info();
Guide::start_time();
for (auto id{0U}; id != max_threads; ++id) {
threads.push_back(std::jthread(workerThread, id));
}
}
可能的运行结果:
Default case: max_threads{10U}, max_sema_threads{3}
Threads: 10, Throughput: 3 │ Legend: initial delay ░░ │ wait state ▒▒ │ sema occupation ██
# 1 ░░░░██████
# 2 ░░░░████████
# 5 ░░░░░░██████████
# 8 ░░░░░░░░░░░░████████████
# 9 ░░░░░░░░░░░░██████████████
# 7 ░░░░░░░░░░░░▒▒▒▒████████████████
# 4 ░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒████████
# 6 ░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒██████████████████
# 3 ░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒████████████
# 0 ░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒██████████████
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
"Enough for everyone" case (no wait states!): max_threads{10U}, max_sema_threads{10}
Threads: 10, Throughput: 10 │ Legend: initial delay ░░ │ wait state ▒▒ │ sema occupation ██
# 4 ░░░░██████
# 5 ░░░░░░████
# 3 ░░░░██████████
# 1 ░░░░██████████
# 8 ░░░░░░░░████████████
# 6 ░░░░░░░░░░░░░░░░██████
# 7 ░░░░░░░░░░░░░░░░██████
# 9 ░░░░░░░░░░░░░░░░██████████
# 0 ░░░░░░░░░░░░██████████████████
# 2 ░░░░░░░░░░░░░░░░░░████████████
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
Binary semaphore case: max_threads{10U}, max_sema_threads{1}
Threads: 10, Throughput: 1 │ Legend: initial delay ░░ │ wait state ▒▒ │ sema occupation ██
# 6 ░░░░████
# 5 ░░░░▒▒▒▒████
# 4 ░░░░░░░░░░▒▒██████████
# 7 ░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒████████████████
# 2 ░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒██████
# 3 ░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒████████████████
# 0 ░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒████████████
# 1 ░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒████████
# 8 ░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒██████
# 9 ░░░░░░░░░░░░░░░░░░▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒██████████████
再看一个二进制状态的例子:
#include <iostream>
#include <thread>
#include <chrono>
#include <semaphore>
using namespace std::literals;
// 全局二元信号量实例
// 设置对象计数为零
// 对象在未被发信状态
std::binary_semaphore smphSignal(0);
void ThreadProc()
{
// 通过尝试减少信号量的计数等待来自主程序的信号
smphSignal.acquire();
// 此调用阻塞直至信号量的计数被从主程序增加
std::cout << "[thread] Got the signal" << std::endl; // 回应消息
// 等待 3 秒以模仿某种线程正在进行的工作
std::this_thread::sleep_for(3s);
std::cout << "[thread] Send the signal\n"; // 消息
// 对主程序回复发信
smphSignal.release();
}
int main()
{
// 创建某个背景工作线程,它将长期存在
std::jthread thrWorker(ThreadProc);
std::cout << "[main] Send the signal\n"; // 消息
// 通过增加信号量的计数对工作线程发信以开始工作
smphSignal.release();
// release() 后随 acquire() 可以阻止工作线程获取信号量,所以添加延迟:
std::this_thread::sleep_for(50ms);
// 通过试图减少信号量的计数等待直至工作线程完成工作
smphSignal.acquire();
std::cout << "[main] Got the signal\n"; // 回应消息
}
运行结果:
[main] Send the signal
[thread] Got the signal
[thread] Send the signal
[main] Got the signal
这种用法和 std::condition_variable有异曲同工的结果,但它的性能更高。
四、总结
c++的进步,有的人说没有什么用,有的人说再怎么进步也不如一些新语言,特别是Rust语言。可是,进步总要比不进步要好吧?大家都有追求幸福的权利,这才真得是公平的世界。

本文探讨了C++20新增的同步数据结构,如信号量、原子等待通知接口、锁存器和屏障,如何提升多线程编程效率,并通过实例展示了它们在实际场景中的应用。
4977

被折叠的 条评论
为什么被折叠?



