四、并发同步
如果一个线程需要等待另一个线程完成某个操作后再执行,可以使用互斥量的方法实现。C++提供了一些更加简单的方式,以满足线程间的同步执行。
4.1 条件变量
条件变量存在于头文件 #include <condition_variable>
中,通过和std::mutex
配合使用,实现一个线程根据特定条件阻塞等待另一个线程。
条件变量使用下列两种成员函数实现线程的同步:
-
阻塞等待指定条件的成员函数
wait
void wait(std::unique_lock<std::mutex>& lock) // 单形参的重载 // 使用方法 while (!some_condition()) { wait(lock); }
- 调用单形参的
wait
时接受已加锁的lock
,内部调用lock.unlock()
后阻塞等待,当其他线程调用对象的notify_one()
或notify_all()
时,内部调用lock.lock()
后执行后续代码 - 阻塞期间可能会因为伪唤醒而调用
lock.lock()
并执行后续代码,因此通常使用下面的重载版本,使得伪唤醒时根据判断条件决定是否跳出阻塞执行后续代码
void wait(std::unique_lock<std::mutex>& lock, // 已加锁的 unique_lock 对象 Predicate pred); // 用于判断条件是否成立的函数
wait
接受的unique_lock
是已加锁的,运行时先调用pred
函数获取返回值- 如果返回
true
,直接去执行后续代码 - 如果返回
false
,则调用lock.unlock()
解锁互斥量,阻塞等待
- 如果返回
- 当有线程调用对象的
notify_one()
或notify_all()
时,阻塞等待的wait
被唤醒,先调用lock.lock()
加锁,再调用pred
获取返回值- 如果返回
true
,去执行后续代码 - 如果返回
false
,调用lock.unlock()
,继续阻塞等待
- 如果返回
wait
执行时有两个需要注意的:- 由于伪唤醒的次数和频率都是不确定的,如果
pred
函数的多次执行会对程序有副作用,应该通过其他方式避免 wait
函数返回后,会继续执行后续的代码,此时lock
仍然是已加锁状态
- 调用单形参的
-
用于通知其他线程等待的条件已满足的成员函数
void notify_one(); // 唤醒一个正在阻塞的 wait void notify_all(); // 唤醒所有正在阻塞的 wait
下面是使用条件变量的一个例子:
std::condition_variable cv; // 条件变量
std::mutex cv_m; // 互斥量
int i = 0;
void waits() {
std::unique_lock<std::mutex> lk(cv_m);
std::cerr << "Waiting... \n";
cv.wait(lk, []{ return i == 1; });
std::cerr << "...finished waiting. i == 1\n";
}
void signals()
{
std::cout << "Sleep one second" << std::endl;
std::this_thread::sleep_for(std::chrono::seconds(1));
{
std::lock_guard<std::mutex> lk(cv_m);
i = 1;
std::cerr << "Notifying again...\n";
}
cv.notify_all();
}
int main() {
std::thread t1(signals), t2(waits), t3(waits);
t1.join();
t2.join();
t3.join();
return 0;
}
/** 输出
Sleep one second
Waiting...
Waiting...
Notifying again...
...finished waiting. i == 1
...finished waiting. i == 1
*/
4.2 使用std::future
执行单次异步任务
1. std::future
std::future
是头文件<future>
中的模板类:
- 由一些异步操作创建并返回,此时该
std::future
对象与该异步操作关联 - 用来保存这些异步操作的返回值或异常,
std::future
是模板对象,模板参数与所关联异步操作的返回值类型相同
std::future
主要包含如下两个常用成员函数:
wait()
:阻塞等待对应的异步操作执行完成get()
:如果异步操作已经完成,会直接返回异步操作的返回值;否则内部调用wait()
阻塞线程直至异步操作执行完成,std::future
变为就绪状态,返回异步操作的结果(优先以右值的方式返回结果)
2. std::async()
函数
std::async()
是头文件<future>
中的函数,用于异步执行指定的可调用对象,包含两个重载:
async(std::launch policy, // 函数在哪个线程执行
F&& f, // 执行的函数
Args&&... args); // 传递给函数的参数
async(F&& f, // 执行的函数
Args&&... args ); // 传递给函数的参数
返回值:
- 返回一个以函数
f
的返回值类型return_type
作为模板参数的std::future<return_type>
对象 - 该
std::future
对象可以用来阻塞线程,等待指定的函数f
执行完成
参数:
policy
:指定函数在哪个线程执行,类型为std::launch
,不同值的含义为:std::launch::async
:启动新线程执行函数std::launch::deferred
:调用返回值std::future
的wait
或get
后在当前线程执行函数(延时调用指定的函数)std::launch::async | std::launch::deferred
:由async
的实现自行选择运行的方式。上面只有两个参数的重载async
就等价于指定了policy
为这个值
template <typename VectorIt>
int ParallelSum(VectorIt beg, VectorIt end) {
auto len = end - beg;
if (len < 1000) {
return std::accumulate(beg, end, 0);
}
auto mid = beg + len / 2;
// 前一半开新线程计算
std::future<int> f_res = std::async(std::launch::async,
ParallelSum<VectorIt>, beg, mid);
// 后一半在当前线程执行
auto sum = std::accumulate(mid, end, 0);
// 前一半的结果 + 后一半的结果
return f_res.get() + sum;
}
std::vector<int> vec(10000, 1);
std::cout << "The sum is " << ParallelSum(vec.begin(), vec.end()) << std::endl;
// The sum is 10000
3. std::packaged_task
对象
std::packaged_task
是头文件<future>
中的模板类,它提供了对需要异步执行的可调用对象的封装,可以实现对异步任务的统一管理。
创建std::packaged_task
的对象时:
- 模板参数指定函数的调用形式(类似于
std::function
,由返回值和函数参数构成,用于描述函数的形式),指明其中所保存的可调用对象的调用形式 - 同时接受一个待执行的可调用对象作为构造函数的参数
std::packaged_task
对象只支持移动语义,其拷贝相关的函数被定义为了删除的
std::packaged_task
中重载了函数调用运算符,所以可以直接通过对象调用所管理的可调用对象
同时std::packaged_task
中包含如下成员函数,用于获取保存可调用对象返回值的std::future
。
std::future<R> get_future();
R
的类型会根据所管理可调用对象的返回值类型确定- 调用返回
std::future
对象的get
函数就可以阻塞等待对应的std::packaged_task
执行完成,并获取执行结果
通过std::packaged_task
及其get_future
,可以获得一对相关联的对象:
packaged_task
对象可以在特定线程中执行- 利用
get_future
获取的future
对象可以在其他地方异步获取执行的结果
下面的代码利用std::packaged_task
的future
异步获取所有的结果:
int AccuVector(std::vector<int>::iterator beg,
std::vector<int>::iterator end) {
return std::accumulate(beg, end, 0);
}
int SliceAccu(std::vector<int>::iterator beg,
std::vector<int>::iterator end) {
// 保存所有 futrue 表示的异步任务的结果
std::vector<std::future<int>> results;
while (beg + 10000 <= end) {
// 依次构造所有任务,并保存任务对应的 future
auto sub_task = std::packaged_task<int(std::vector<int>::iterator,
std::vector<int>::iterator)>(AccuVector);
results.push_back(sub_task.get_future());
// 将任务放到特定线程中去执行
std::thread task_td(std::move(sub_task), beg, beg + 10000);
task_td.detach();
// 新任务的起点
beg += 10000;
}
int sum = 0;
// 当前线程只利用所有 future 获取结果
for (auto &res : results) {
sum += res.get();
}
return sum;
}
std::vector<int> vec(50000, 1);
std::cout << "The sum is " << SliceAccu(vec.begin(), vec.end()) << std::endl;
// The sum is 50000
4. std::promise
std::promise
用于在不同线程间传递值,且只能使用一次。
创建std::promise
时需要通过模板参数指定传递的值类型:
std::promise<int> pro_test;
std::promise
的拷贝相关的函数被定义为了删除的,只能执行移动操作
std::promise
包含一个get_future
函数,可以获取用于保存所传递值的future
:
std::future<int> res_future = pro_test.get_future();
std::promise
可以使用成员函数set_value
,将对应类型的值传递给预先返回的future
对象:
pro_test.set_value(val);
- 创建
std::promise
时,模板参数可以为void
,此时对应的std::future
的模板参数也为void
,则调用set_value
时不应该传递任何参数(这通常用来作为某件事情的通知,并不传递实际的数据)
当使用预先获取的future
对象试图获取传递的值时,如果std::promise
对象还没有调用set_value
,程序会阻塞,直至set_value
被调用:
// 阻塞等待 set_value 调用
int res = res_future.get();
下面展示了使用std::promise
在线程间传递值的方法:
void AccuVector(std::vector<int>::iterator beg,
std::vector<int>::iterator end,
std::promise<int> accu_promise) {
// 将数据累加
int sum = std::accumulate(beg, end, 0);
// 使用 std::promise 将数据传给 futrue
accu_promise.set_value(sum);
}
// 划分任务
int SliceAccu(std::vector<int>::iterator beg,
std::vector<int>::iterator end) {
// 保存所有 futrue 表示的异步任务的结果
std::vector<std::future<int>> results;
while (beg + 10000 <= end) {
// 依次构造所有任务,并保存任务对应的 future
std::promise<int> cur_accu_promise;
// 保存可以获取结果的 future
results.push_back(cur_accu_promise.get_future());
// 将特定范围的数据异步去执行
std::thread task_td(AccuVector, beg, beg + 10000, std::move(cur_accu_promise));
task_td.detach();
// 新任务的起点
beg += 10000;
}
int sum = 0;
// 当前线程只利用所有 future 获取结果
for (auto &res : results) {
sum += res.get();
}
return sum;
}
std::vector<int> vec(50000, 1);
std::cout << "The sum is " << SliceAccu(vec.begin(), vec.end()) << std::endl;
// The sum is 50000
5. std::future
保存异常
std::future
都会关联到某个异步操作,如std::async
、std::packaged_task
、std::promise
等,但是在异步执行特定操作时,可能会抛出异常。C++定义,可以使用std::future
保存对应异步操作所抛出异常。
std::async
、std::packaged_task
所调用的函数抛出异常时,异常对象会保存到所关联的std::future
中;当调用std::future
的get
函数时,异常会重新抛出std::promise
由于并不直接调用异步操作的相关语句,如果想要向所关联的std::future
中保存异常,不能调用set_value
函数,应该直接调用成员函数set_exception()
,这样在关联的std::future
调用get
时异常也会重新抛出void set_exception(std::exception_ptr p);
set_exception
接受一个std::exception_ptr
类型的对象,该类型的对象可以通过如下两种方法获得// 1. 使用 std::make_exception_ptr(E e) cur_promise.set_exception(std::make_exception_ptr(std::logic_error("error"))); // 2. 使用 std::current_exception 在 catch 子句中根据捕获的异常创建, // 返回一个 std::exception_ptr 对象 try { throw std::runtime_error("runtime error"); } catch (...) { // 这里也可以指定特定类型的异常,current_exception都可以获取到 pro.set_exception(std::current_exception()); }
另外,对于std::packaged_task
与std::promise
的std::future
,如果std::future
在没有get
获取结果前,std::packaged_task
与std::promise
的对象被销毁了,则对象的析构函数会将保存了std::future_errc::broken_promise
码的std::future_error异常对象保存到std::future
中,通过异常对象的code
成员函数可以获取到异常码。
使用std::future
保存异常的示例:
void AsyncSaveExcepion() {
throw std::runtime_error("runtime error async");
}
void PromiseSaveException(std::promise<void> &pro) {
// 调用 std::make_exception_ptr 创建 exception_ptr 对象
pro.set_exception(std::make_exception_ptr(std::runtime_error("runtime error promise")));
}
int main(int argc, char const *argv[])
{
// async 获取异常
std::future<void> async_exception = std::async(AsyncSaveExcepion);
try {
async_exception.get();
} catch (std::runtime_error &err) {
std::cout << "async exception: " << err.what() << std::endl;
}
// promise 获取异常
std::promise<void> pro;
std::future<void> promise_exception = pro.get_future();
// 执行线程函数
std::thread t(PromiseSaveException, std::ref(pro));
try {
promise_exception.get();
} catch (std::runtime_error &err) {
std::cout << "proimse exception: " << err.what() << std::endl;
}
t.join();
return 0;
}
/**输出
async exception: runtime error async
proimse exception: runtime error promise
*/
6. std::shared_future
共享异步结果
std::future
只有移动语义,如果想在多个线程中同时获取std::future
对应异步操作的结果,要使用std::shared_future
。
创建std::shared_future
主要包含如下两种方式:
shared_future(std::future<T>&& other)
:使用std::future
的右值创建,此时对应的other
对象将关联的异步操作转移到新的shared_future
对象中,other
对象变为空std::shared_future<T> future<T>::shared()
:std::future
对象的shared()
成员函数会返回一个shared_future
对象,此操作也会转移std::future
所关联的异步操作,原std::future
对象会变为空
std::shared_future
的使用方法和std::future
相同:
- 使用
wait
阻塞等待异步操作执行完成 - 使用
get
获得异步操作的返回值
std::shared_future
是可以复制的,当std::shared_future
关联的异步操作执行结束并返回结果后,所有等待的shared_future
对象都会处于就绪状态,它们都会获得对应异步操作的返回值
下面展示了利用shared_future
在多个线程获取结果:
std::chrono::time_point<std::chrono::high_resolution_clock> start;
std::chrono::duration<double, std::milli>
WaitFuture1(std::shared_future<void> ready_future) {
ready_future.wait();
return std::chrono::high_resolution_clock::now() - start;
}
std::chrono::duration<double, std::milli>
WaitFuture2(std::shared_future<void> ready_future) {
ready_future.wait();
return std::chrono::high_resolution_clock::now() - start;
}
int main(int argc, char const *argv[])
{
std::promise<void> pro;
std::shared_future<void> ready_future(pro.get_future());
std::future res1 = std::async(WaitFuture1, ready_future);
std::future res2 = std::async(WaitFuture2, ready_future);
// 记录开始时间
start = std::chrono::high_resolution_clock::now();
// 将所关联的 shared_future 转换为就绪态
pro.set_value();
std::cout << "WaitFuture1 received the signal after "
<< res1.get().count() << "ms" << std::endl;
std::cout << "WaitFuture2 received the signal after "
<< res2.get().count() << "ms" << std::endl;
return 0;
}
/**输出
WaitFuture1 received the signal after 0.055026ms
WaitFuture2 received the signal after 0.049686ms
*/
4.3 有限等待
1. sleep_for
与sleep_until
std::this_thread::sleep_until(point)
:接受一个std::chrono::time_point
时间点类型的函数point
,阻塞等待至该指定的时间后,继续执行后续代码
- 通常使用
Clock::now() + sleep_time
获取目标的时间点 - 由于进程调度或资源争用,实际唤醒的时间可能会大于该时间点
std::this_thread::sleep_for(dur)
:接受一个std::chrono::duration
时长类型的参数dur
,阻塞等待dur
时间后,继续执行后续代码
- 由于进程调度或资源争用,实际阻塞的时长可能会大于指定的时长
2. condition_variable
的限时等待
对于一个条件变量对象,正常情况下,调用wait
后会一直阻塞,直至在其他线程中调用了notify_one
或notify_all
但是在有些场景下,可能需要限制等待的时间,即阻塞时间如果超过指定的时间,则跳出阻塞,并返回相关的状态
condition_variable
的限时等待包含两个成员函数:
wait_until
:阻塞等待,直至notify_xxx
被调用或到达指定的时间点。与wait
相同,此函数也包含两个重载std::cv_status wait_until(unique_lock, time_point)
- 调用
lock.unlock()
阻塞等待,当调用notify_one
或调用notify_all
或到达指定时间点time_point
或发生伪唤醒时,执行lock.lock()
后继续运行代码。
bool wait_until(unique_lock, time_point, pred)
- 函数被唤醒时,都会先执行
lock.lock()
,然后根据pred()
的值,确定要调用lock.unlock()
阻塞程序还是调用lock.lock()
执行后续代码
- 调用
wait_for
:std::cv_status wait_for(unique_lock, rel_time)
- 阻塞等待,当调用
notify_one
或调用notify_all
或阻塞时长超过rel_time
或发生伪唤醒时,执行lock.lock()
后继续运行后续代码
bool wait_for(unique_lock, rel_time, pred)
- 函数被唤醒时,都会先执行
lock.lock()
,然后根据pred()
的值,确定要调用lock.unlock()
阻塞程序还是调用lock.lock()
执行后续代码
- 阻塞等待,当调用
返回值:
std::cv_status
是一个枚举类,包含两个成员:std::cv_status::timeout
表示函数因超过等待的时间而返回std::cv_status::no_timeout
表示函数并非因超时而返回
- 返回
bool
的wait_until
和wait_for
函数,其返回值和最后一次调用pred()
时的值相同
下面的代码展示了使用wait_until
等待特定的时间:
#include <chrono>
#include <condition_variable>
#include <iostream>
#include <mutex>
#include <random>
#include <thread>
#include <vector>
using namespace std::literals::chrono_literals;
std::mutex mut;
std::condition_variable cv;
int wait_value = 0;
void wait_random_time() {
// 创建随机数发生器
static std::default_random_engine e;
static std::uniform_int_distribution<int> uni_real(2500, 4000);
// 获取当前时间
auto start_time = std::chrono::steady_clock::now();
// 计算延迟随机时间后,得到的新时间点
std::chrono::steady_clock::time_point dis_time =
start_time + std::chrono::milliseconds(uni_real(e));
// 阻塞等待
std::unique_lock<std::mutex> uni_lock(mut);
bool res = cv.wait_until(uni_lock, dis_time, [] { return wait_value == 1; });
// 根据 res 确定是超时返回还是 notify 唤醒
if (res) {
std::cout << "[" << std::this_thread::get_id()
<< "]: return because of notify";
} else {
std::cout << "[" << std::this_thread::get_id()
<< "]: return because of timeout";
}
}
/** @brief 3秒后调用 notify_all */
void notify_after_3_sec() {
// 等待 3s
std::this_thread::sleep_for(3s);
// 锁住互斥量,修改变量值
std::lock_guard lk(mut);
wait_value = 1;
// 唤醒所有等待条件的线程
cv.notify_all();
}
int main(int argc, char const *argv[]) {
// 保存所有的线程
std::vector<std::thread> threads;
// 创建五个随机等待的线程
for (int i = 0; i < 5; ++i) {
threads.emplace_back(wait_random_time);
}
// 创建一个用于唤醒的线程
std::thread signal_thread(notify_after_3_sec);
// join 所有线程
for (int i = 0; i < 5; ++i) {
threads[i].join();
}
signal_thread.join();
return 0;
}
/**
[140737348179520]: return because of timeout
[140737339786816]: return because of timeout
[140737331394112]: return because of notify
[140737323001408]: return because of notify
[140737314608704]: return because of notify
*/
3. 可以指定超时的互斥量
对于之前介绍的互斥量std::mutex
、std::recursive_mutex
和std::shared_mutex
,它们只能使用lock
阻塞等待加锁或try_lock
不阻塞地判断能否加锁,为了限制互斥量的阻塞时间,C++分别提供了timed
的互斥量。
<mutex>
头文件中提供了:
std::timed_mutex与std::recursive_timed_mutex类型,分别对应于std::mutex
与std::recursive_mutex
,它们除了包含std::mutex
包含的所有操作外,还包含两个限制阻塞时间的加锁函数:
-
bool try_lock_until(time_point)
阻塞等待对互斥量加锁成功,如果到达指定的
time_point
还没能成功加锁,返回false
;否则返回true
-
bool try_lock_for(duration)
阻塞等待对互斥量加锁成功,如果阻塞时长达到
duration
还没有成功加锁,返回false
;否则返回true
类似的,在头文件<shared_mutex>
中还提供了std::shared_timed_mutex类型,对应于shared_mutex
。除了提供shared_mutex
包含的所有操作外,还提供了如下函数用于限制阻塞时间。
限制互斥锁的等待时长:
bool try_lock_until(time_point)
bool try_lock_for(duration)
限制共享锁的等待时长
bool try_lock_shared_until(time_point)
bool try_lock_shared_for(duration)
同样,管理互斥量的std::unique_lock
与std::shared_lock
也都包含了指定阻塞时长的函数,这些函数会调用所管理互斥量的对应函数,实现限制阻塞时的等待时长。其参数和返回值表达的含义与互斥量对应的函数一样:
bool try_lock_until(time_point)
bool try_lock_for(duration)
#include <chrono>
#include <iostream>
#include <mutex>
#include <sstream>
#include <thread>
#include <vector>
std::timed_mutex t_mut;
void try_lock_for(int id) {
// 等待固定的时间
std::unique_lock<std::timed_mutex> uni_timed_mut(t_mut, std::defer_lock);
// 保存所有输出的字符串
std::ostringstream str;
for (int i = 0; i < 2; ++i) {
// 阻塞等待最多 50ms
bool res = uni_timed_mut.try_lock_for(std::chrono::milliseconds(50));
if (res) {
str << "success ";
// 等待 50ms
std::this_thread::sleep_for(std::chrono::milliseconds(50));
// 解锁互斥量
uni_timed_mut.unlock();
} else {
str << "failed ";
}
}
std::cout << "[" << id << "]" << str.str() << std::endl;
}
int main(int argc, char const *argv[]) {
std::vector<std::thread> threads;
for (int i = 0; i < 5; ++i) {
threads.emplace_back(try_lock_for, i);
}
for (auto &th : threads) {
th.join();
}
return 0;
}
/**
[0]success success
[2]failed failed
[3]failed failed
[4]failed failed
[1]failed success
*/
4. 限制std::future
的等待时间
std::future
和std::shared_future
类型中都定义了如下两个成员函数用于限制wait
等待的时长或时间点:
std::future_status wait_until(time_point)
std::future_status wait_for(duration)
其返回值类型std::future_status
是一个枚举类型,包含成员:
std::future_status::deferred
:表示所关联的异步操作的函数等待执行std::future_status::ready
:表示所关联的异步操作已执行完成std::future_status::timeout
:表示阻塞等待超时
#include <chrono>
#include <future>
#include <iostream>
#include <random>
#include <thread>
#include <vector>
#include <ctime>
int main(int argc, char const *argv[]) {
// 保存所有 future
std::future<int> future;
future = std::async(std::launch::async, []() {
std::this_thread::sleep_for(std::chrono::milliseconds(300));
return 0;
});
// 每次等待 100ms 检查执行状态,共检查三次
std::future_status res;
int check_times = 3;
while (check_times--) {
res = future.wait_for(std::chrono::milliseconds(100));
std::cout << "The " << 3 - check_times << "th check: ";
switch (res) {
case std::future_status::deferred:
std::cout << "deferred\n";
break;
case std::future_status::timeout:
std::cout << "timeout\n";
break;
case std::future_status::ready:
std::cout << "ready\n";
break;
}
}
std::cout << "res: " << future.get() << "\n";
return 0;
}
/**
The 1th check: timeout
The 2th check: timeout
The 3th check: ready
res: 0
*/
4.4 latch
和barrier
1. std::latch
std::latch在C++20被引入,在头文件<latch>
中,是一个用来线程同步的对象,使用时设置一个计数器,线程可以阻塞等待计数器值减少为0后,再继续执行后续代码。
std::latch
对象的构造函数:
explicit latch(std::ptrdiff_t expected);
expected
:指定计数器的初始值
在需要等待计数器值减少为0的线程中,可以使用wait
函数阻塞等待:
latch.wait();
对于完成特定任务的线程,可以使用count_down
函数对计数器减少指定的数值:
void count_down(std::ptrdiff_t n = 1);
n
:对计数器的减少的数值,默认值为1- 对计数器值的减少是原子操作,不会因为多个线程同时操作而发生数据错误
- 对计数器减少的值如果是负数或大于内部计数器的值,会产生未定义的行为
有些线程可能需要先减少计数器的值,再阻塞等待计数器值变为0,这时可以使用简化的函数:
void arrive_and_wait(std::ptrdiff_t n = 1);
n
:内部计数器要减少的值- 该函数对计数器的减少也是原子操作
std::latch
的计数器值不可以重置,也不能增加,所以只能使用一次。但是可以在一个线程中多次减少计数器的值。
std::latch work_done{5};
void WorkThread() {
static std::default_random_engine e;
static std::uniform_int_distribution<int> uni_int(3, 6);
int wait_time = uni_int(e);
std::cout << "[" << std::this_thread::get_id() << "] wait for "
<< wait_time << "s" << std::endl;
// 休眠若干秒,模拟耗时操作
std::this_thread::sleep_for(std::chrono::seconds(wait_time));
// 减少计数器的值
work_done.count_down();
}
int main(int argc, char const *argv[]) {
std::vector<std::thread> threads;
// 记录开始时间
auto beg_time = std::chrono::steady_clock::now();
for (int i = 0; i < 5; ++i) {
threads.push_back(std::thread(WorkThread));
}
// 等待所有线程执行完成
work_done.wait();
// 计算等待时长
auto dura = std::chrono::duration_cast<std::chrono::milliseconds>
(std::chrono::steady_clock::now() - beg_time);
std::cout << "main thread wait for " << dura.count() << "ms" << std::endl;
for (int i = 0; i < 5; ++i) {
threads[i].join();
}
return 0;
}
/** 输出
[140737348179520] wait for 3s
[140737339786816] wait for 3s
[140737331394112] wait for 6s
[140737323001408] wait for 4s
[140737314608704] wait for 5s
main thread wait for 6001ms
*/
2. std::barrier
头文件<barrier>
中的std::barrier
也提供了一种多个线程阻塞等待计数器值减少为0的功能,但是它相比于std::latch
:
- 可以在计数器减少为0时自动执行指定函数
- 当计数器减少为0解除阻塞后,可以重新开始新一轮的等待
- 每一轮都可以调整下一轮的计数器的初始值
std::barrier
的构造函数:
explicit barrier(std::ptrdiff_t expected,
CompletionFunction f = CompletionFunction());
expected
:指定计数器的初始值f
:当计数器的值减少到0后,会首先调用该函数,默认是一个空的可调用对象
减少计数器值与阻塞的函数:
arrival_token arrive(std::ptrdiff_t n = 1); // 减少计数器值,并返回标识当前轮次的 arrival
void wait(arrival_token&& arrival); // 阻塞等待
arrive
:函数可以指定减少的计数器值大小(原子操作),并返回一个与当前轮次相关联的标志wait
:接受一个标记了某个轮次的标志,并在该轮次阻塞等待计数器值变为0- 通常两者一起使用:
wait(arrive());
为了简化以上过程,C++提供了下面的简化成员函数:
void arrive_and_wait();
- 原子性的将计数器的值减1,并在当前轮次阻塞等待计数器变为0,等价于
wait(arrive())
std::barrier
是可以重复使用的,一般情况下,当计数器值变为0后,会重置计数器的值,并开始下一轮次的arrive
和wait
。C++还提供了可以调整下一轮计数器值初始值的方法:
void arrive_and_drop()
- 将当前轮次的计数器值减少1,并阻塞等待计数器值变为0
- 指定后续所有轮次计数器的初始值减少1
综上所述,std::barrier
每轮的运行过程为:
- 使用初始值初始化计数器
- 调用
arrive_and_wait()
或arrive_and_drop()
将当前计数器值减1并阻塞 - 当某个线程调用
arrive_xx
后如果计数器变为0,在该线程执行创建对象时指定的函数f
,之后解除所有的阻塞等待 - 根据调用
arrive_and_drop
的次数确定下一轮运行时计数器的初始值,并开始新一轮的运行过程
示例:
// 保存每轮生成的数字
std::vector<int> cur_num{4};
// 保存当前的轮次
int cur_phase = 1;
// 所有生成数字的累加和
int sum_num = 0;
/** @brief 在每个轮次计算本轮生成的所有数字和,用以确认是否结束线程 */
void ComplateFun() {
std::cout << "[" << std::this_thread::get_id() << "]: ";
for (int i = 0; i < 4; ++i) {
sum_num += cur_num[i];
}
// 根据结果,输出不同的内容
if (sum_num > 40) {
std::cout << "end all phase, sum_num: " << sum_num << "\n";
} else {
std::cout << "sum_num: " << sum_num
<< "\n...begin phase " << cur_phase++ << " ...\n";
}
}
// 用于同步的 barrier
std::barrier sync_bar{4, ComplateFun};
/**
* @brief 每个线程执行的函数,当 sum_num 达到 40 时退出线程
* @param thread_id 标识每个线程,并指定该线程生成的数字保存到 cur_num 的哪个位置
*/
void WorkFunc(int thread_id) {
static std::default_random_engine e;
static std::uniform_int_distribution<int> uin_int(3, 8);
// 当 sum_num 达到 40 及以上时退出线程
while (sum_num < 40) {
// 随机生成一个整数
cur_num[thread_id] = uin_int(e);
std::cout << "[" << std::this_thread::get_id() << "]: gen num "
<< cur_num[thread_id] << "\n";
// 根据生成的数字设置阻塞时长
std::this_thread::sleep_for(std::chrono::seconds(cur_num[thread_id]));
// 阻塞等待本轮所有数字都生成完成
sync_bar.arrive_and_wait();
}
}
int main(int argc, char const *argv[]) {
std::cout << "...begin phase " << cur_phase++ << " ...\n";
// 开启所有线程
std::vector<std::thread> threads;
for (int i = 0; i < 4; ++i) {
threads.push_back(std::thread(WorkFunc, i));
}
// 释放所有线程
for (int i = 0; i < 4; ++i) {
threads[i].join();
}
return 0;
}
/**
...begin phase 1 ...
[140737348179520]: gen num 3
[140737339786816]: gen num 3
[140737331394112]: gen num 7
[140737323001408]: gen num 5
[140737331394112]: sum_num: 18
...begin phase 2 ...
[140737331394112]: gen num 6
[140737348179520]: gen num 4
[140737323001408]: gen num 7
[140737339786816]: gen num 3
[140737323001408]: sum_num: 38
...begin phase 3 ...
[140737323001408]: gen num 7
[140737339786816]: gen num 8
[140737348179520]: gen num 5
[140737331394112]: gen num 6
[140737339786816]: end all phase, sum_num: 64
*/