Chapter 4 并发同步操作 · 归纳总结
来源:simonhancrew.github.io/CppConcuren... (对应《C++ Concurrency in Action》第 4 章"Concurrent Synchronization Operations") 本章主题:线程不仅需要保护共享数据,更需要"同步" ------ 等待事件、获取结果、限定时间等待、以及如何用同步操作把并发代码写得更简单。
结构:① 等待事件/条件(条件变量)→ ② 使用 future(获取结果)→ ③ 限时等待 → ④ 用同步简化代码(FP/CSP/experimental)。
0. 为什么需要"同步"
线程之间光保护共享数据(mutex)不够,常常要等一个事件/条件达成。可能的做法与优劣:
| 做法 | 实现 | 缺点 |
|---|---|---|
| 忙轮询 | 循环读共享标志 | 浪费 CPU,若持锁会阻塞其他线程 |
| 周期休眠轮询 | sleep_for |
休眠时长难定:太短浪费、太长延迟 |
| 条件变量(推荐) | notify + wait |
等待时解锁、被唤醒检查、不浪费执行时间 |
1. 等待事件或条件(4.1)
1.1 两种条件变量
| 类型 | 搭配 | 特点 |
|---|---|---|
std::condition_variable |
只能配 std::mutex |
性能好、资源省,首选 |
std::condition_variable_any |
任意互斥量 | 更灵活,但更耗资源 |
均在 <condition_variable> 头文件,都要配合互斥量使用。
1.2 忙碌-等待的"最小骨架
cpp
template<typename Predicate>
void minimal_wait(std::unique_lock<std::mutex>& lk, Predicate pred){
while(!pred()){
lk.unlock();
lk.lock();
}
}
1.3 生产-消费者:通知 + wait(代码 4.1)
cpp
std::mutex mut;
std::queue<data_chunk> data_queue; // 1 受保护的数据
std::condition_variable data_cond;
void data_preparation_thread(){ // 生产者
while(more_data_to_prepare()){
data_chunk const data=prepare_data();
std::lock_guard<std::mutex> lk(mut);
data_queue.push(data);
data_cond.notify_one(); // 3 通知等待者
}
}
void data_processing_thread(){ // 消费者
while(true){
std::unique_lock<std::mutex> lk(mut); // 4 必须 unique_lock
data_cond.wait(lk,[]{return !data_queue.empty();}); // 5 等 predicate 为真
data_chunk data=data_queue.front();
data_queue.pop();
lk.unlock(); // 6 处理时提前解锁
process(data);
if(is_last_chunk(data)) break;
}
}
关键点 / 为什么用 unique_lock 而非 lock_guard:
- 等待线程必须等待期间解锁互斥量、被唤醒后重新加锁 。
lock_guard无法做到,unique_lock可以。 若等待时还锁着锁,生产者无法入队 → waiting线程永远等不到条件达成。 - 处理数据时
lk.unlock()提前放锁,避免长时间持锁。 - 伪唤醒(spurious wakeup) :
wait()可能没被 notify 也醒来;所以一定要用谓词形式 ,且谓词最好无副作用。
1.4 线程安全队列:接口(代码 4.3)
cpp
template<typename T>
class threadsafe_queue{
public:
threadsafe_queue();
threadsafe_queue(const threadsafe_queue&);
threadsafe_queue& operator=(const threadsafe_queue&) = delete; // 禁赋值
void push(T new_value);
bool try_pop(T& value); // ① 引用存值,返回bool
std::shared_ptr<T> try_pop(); // ② 直接返回shared_ptr,空时null
void wait_and_pop(T& value);
std::shared_ptr<T> wait_and_pop();
bool empty() const;
};
- 把
front()+pop()合并成一次调用,避免接口层条件竞争。 try_pop:不管有没有都立刻返回;wait_and_pop:等到有值才返回。
1.5 完整版线程安全队列(代码 4.5)
cpp
#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>
template<typename T>
class threadsafe_queue{
private:
mutable std::mutex mut; // 1 mutable ------ empty()/拷贝构造是const时才可锁
std::queue<T> data_queue;
std::condition_variable data_cond;
public:
threadsafe_queue(){}
threadsafe_queue(threadsafe_queue const& other){
std::lock_guard<std::mutex> lk(other.mut); // 拷贝要锁别人的锁
data_queue=other.data_queue;
}
void push(T new_value){
std::lock_guard<std::mutex> lk(mut);
data_queue.push(new_value);
data_cond.notify_one();
}
void wait_and_pop(T& value){
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk,[this]{return !data_queue.empty();});
value=data_queue.front(); data_queue.pop();
}
std::shared_ptr<T> wait_and_pop(){
std::unique_lock<std::mutex> lk(mut);
data_cond.wait(lk,[this]{return !data_queue.empty();});
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop(); return res;
}
bool try_pop(T& value){
std::lock_guard<std::mutex> lk(mut);
if(data_queue.empty()) return false;
value=data_queue.front(); data_queue.pop(); return true;
}
std::shared_ptr<T> try_pop(){
std::lock_guard<std::mutex> lk(mut);
if(data_queue.empty()) return std::shared_ptr<T>();
std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
data_queue.pop(); return res;
}
bool empty() const{
std::lock_guard<std::mutex> lk(mut);
return data_queue.empty();
}
};
要点(tip)
mut要标mutable:因为empty()是const成员、拷贝构造参数是const&,都得对互斥量上锁。- 赋值运算符
=delete直接禁止,避免非线程安全的默认赋值。
1.6 notify_one vs notify_all
notify_one():只唤醒一个正在等待的线程。适用task分给其中一个就能处理。notify_all():唤醒所有等待线程。适用数据初始化/周期性重初始化等多消费者都要响应。- 条件为 true 时等待线程只等一次就不等了 → 等待一组可用数据块场景,条件变量非最优 → 正文见 4.2 的 future。
2. 使用 future(4.2)
核心概念 (候机登机广播比喻):future 表示一个唯一的一次性事件,线程可去等它或先做别的,就绪后取结果。一旦就绪不可重置。
std::future<>:只能与一个事件关联(唯一所有权)。std::shared_future<>:可共享给多个,所有实例同时就绪。- 无关数据时用
std::future<void>/std::shared_future<void>特化。 - future 本身不提供同步访问(要锁);但多线程各自持
shared_future副本则无需锁。 - 并发规范在
std::experimental扩展,需<experimental/future>。
2.1 std::async 后台取返回值(代码)
cpp
#include <future>
int find_the_answer_to_ltuae();
void do_other_stuff();
int main(){
std::future<int> the_answer=std::async(find_the_answer_to_ltuae);
do_other_stuff(); // 先做别的
std::cout<<"The answer is "<<the_answer.get()<<; // get() 阻塞等结果
}
传参与 std::thread 类似------成员函数指针/对象、拷贝/引用/右值移动:
cpp
auto f1=std::async(&X::foo,&x,42,"hello"); // 调用 p->foo(...) p指向x
auto f2=std::async(&X::bar,x,"goodbye"); // 拷贝x再调用
auto f3=std::async(Y(),3.141); // 临时对象(移动构造)
auto f4=std::async(std::ref(y),2.718); // 引用y
auto f5=std::async(move_only()); // 移动语义
执行方式 flag ------ std::launch:
cpp
std::async(std::launch::async, Y(), 1.2); // 单新线程上执行
std::async(std::launch::deferred, baz, std::ref(x)); // 等到 wait()/get() 才执行
std::async(std::launch::deferred | std::launch::async, // 实现自由选择(默认)
baz, std::ref(x));
2.2 std::packaged_task<> ------ 把 future 绑定到可调用对象
packaged_task<>把函数/可调用对象与 future 绑定;调用该对象时结果存入 future,并置为就绪。- 用于线程池/任务调度。
cpp
template<>
class packaged_task<std::string(std::vector<char>*,int)>{
public:
template<typename Callable> explicit packaged_task(Callable&& f);
std::future<std::string> get_future(); // 取 future
void operator()(std::vector<char>*,int); // 执行,返回值存 future
};
GUI 线程任务队列示例 :轮询队列出任务执行、packaged_task<void()> 处理无参无声任务。
cpp
std::deque<std::packaged_task<void()>> tasks;
void gui_thread(){
while(!gui_shutdown_message_received()){
get_and_process_gui_message();
std::packaged_task<void()> task;
{ std::lock_guard<std::mutex> lk(m);
if(tasks.empty()) continue;
task=std::move(tasks.front()); tasks.pop_front(); }
task(); // 执行
}
}
template<typename Func>
std::future<void> post_task_for_gui_thread(Func f){
std::packaged_task<void()> task(f);
std::future<void> res=task.get_future();
{ std::lock_guard<std::mutex> lk(m); tasks.push_back(std::move(task)); }
return res;
}
2.3 std::promise<T> ------ 显式设置值
get_future()拿关联 future;set_value()后 future 就绪可取值。- promise 在设值前被销毁 → 存一个异常(broken_promise)。
cpp
std::promise<int> some_promise;
some_promise.set_value(42);
std::future<int> f=some_promise.get_future(); // 或用 get_future 先取
int x=f.get();
2.4 把异常存入 future
std::async/std::packaged_task执行抛异常 → 存进 future,get()会重新抛。- promise 用
set_exception():
cpp
extern std::promise<double> some_promise;
try { some_promise.set_value(calculate_value()); }
catch(...){ some_promise.set_exception(std::current_exception()); }
// 或直接存:
some_promise.set_exception(std::copy_exception(std::logic_error("foo")));
- promise/packaged_task 析构时若 future 未就绪 → 存
std::future_error(broken_promise);丢掉 promise 但没设置会让等待线程一直等。
2.5 std::shared_future ------ 多线程同时等待
std::future只移动、只能一个取结果、get()只能一次。std::shared_future可拷贝,多对象指同一同步结果。对特循环,推推推荐每线程持有自己的拷贝。
cpp
std::promise<int> p;
std::future<int> f(p.get_future());
assert(f.valid());
std::shared_future<int> sf(std::move(f)); // 转移所有权
assert(!f.valid());
assert(sf.valid());
// 隐式转移 + share() 自动推导
std::promise<std::map<...>::iterator> p2;
auto sf2=p2.get_future().share();
3. 限时等待(4.3)
3.1 超时两种指定方式
- 指定时间段(duration) → 等待函数以
_for结尾,如wait_for(30ms)。 - 指定绝对时间点(time_point) →
_until结尾,如wait_until(timeout)。
3.2 时钟(Clocks)四要素
- 当前时间:
now() - 时间点类型:
time_point - 节拍:
std::ratio<1,x> - 稳定性:
is_steady(true=稳定)
三常用时钟:
| 时钟 | 性质 |
|---|---|
system_clock |
系统实时间,不稳定(可被调整) |
steady_clock |
稳定,适合算超时 |
high_resolution_clock |
通常精度最高 |
3.3 时间段 durations
std::chrono::duration<Rep,Ratio>;预定义:nanoseconds..hours。C++14 字面量:
cpp
using namespace std::chrono_literals;
auto one_day=24h; auto half=30min; auto max_wait=30ms;
duration_cast 截断而非四舍五入:
cpp
std::chrono::milliseconds ms(54802);
auto s=std::chrono::duration_cast<std::chrono::seconds>(ms); // 54
计时:
cpp
auto start=std::chrono::high_resolution_clock::now();
do_something();
auto stop=std::chrono::high_resolution_clock::now();
std::cout << std::chrono::duration<double,std::chrono::seconds>
(stop-start).count() << "s\n";
3.4 future 限时等待
cpp
std::future<int> f=std::async(some_task);
if(f.wait_for(std::chrono::milliseconds(35))==std::future_status::ready)
do_something_with(f.get());
// future_status: ready / timeout / deferred(任务尚未启动)
3.5 条件变量绝对时间点限时(代码 4.11) ------ 关键
cpp
bool wait_loop(){
auto const timeout=std::chrono::steady_clock::now()+std::chrono::milliseconds(500);
std::unique_lock<std::mutex> lk(m);
while(!done){
if(cv.wait_until(lk,timeout)==std::cv_status::timeout) break;
}
return done;
}
⚠ 重要 :用
wait_for()的循环在假唤醒 时可能"无限期"延 ------ 每次都在醒来被唤醒、重新计时再等。 改用绝对时间点wait_until才能限制整轮循环总时长。
3.6 可接受超时的函数汇总(表4.1)
| 命名空间 | 函数 | 返回 |
|---|---|---|
std::this_thread |
sleep_for(d) / sleep_until(tp) |
--- |
condition_variable/_any |
wait_for/wait_until(lock[,pred]) |
cv_status;带谓词返回 bool |
timed_mutex() |
try_lock_for/until |
bool |
unique_lock<TimedLockable> |
构造(d/t) 或 try_lock_for/until |
bool/owns_lock() |
future/shared_future |
wait_for/until |
future_status |
睡眠只是超时之一;超时还可用于互斥锁获取、条件变量、future 等待。
4. 用同步操作简化代码(4.4)
4.4.1 函数式编程(future)无竞争
纯函数只依赖参数、不改外部状态 → 无数据竞争、无需互斥锁。C++11 lambda/
std::bind/auto 推导 + future 使 FP 并发成为可能。
快速排序串行版(4.12):
cpp
template<typename T>
std::list<T> seq_sort(std::list<T> input){
if(input.empty()) return input;
std::list<T> result;
result.splice(result.begin(),input,input.begin()); // 取左边开头作 pivot
T const& pivot=*result.begin();
auto divide=std::partition(input.begin(),input.end(),
[&](T const& t){return t<pivot;});
std::list<T> lower; lower.splice(lower.end(),input,input.begin(),divide);
auto nl=seq_sort(std::move(lower));
auto nh=seq_sort(std::move(input));
result.splice(result.end(),nh);
result.splice(result.begin(),nl);
return result;
}
并行版(4.13): 把 纯递归(lower) 换成 std::async(¶llel_quick_sort,std::move(lower)) → 返回 future。大任务同步裁,小的部分用新线程 → 运行库.并发时会递归多线程,任务过多时可能自动转同步(默认策略)。
spawn_task(4.14): 用 packaged_task 包装然后 detach 返回 future:
cpp
template<typename F,typename A>
std::future<std::result_of<F(A&&)>::type> spawn_task(F&& f,A&& a){
typedef std::result_of<F(A&&)>::type res;
std::packaged_task<res(A&&)> task(std::move(f));
std::future<res> r(task.get_future());
std::thread(std::move(task),std::move(a)).detach();
return r;
}
4.4.2 消息传递------CSP/参与者
CSP(communicationSequential Process,Hoare 1985):线程无共享数据、通过通讯渠道传消息,各自变状态机;C++ 共享地址空间,靠约定确保不共享,C传消息封装入库。
ATM 状态机(4.15/4.16):
cpp
class atm{
messaging::receiver incoming; sender bank, interface;
void (atm::*state)();
std::string account, pin;
void waiting_for_card(){
interface.send(display_enter_card());
incoming.wait().handle<card_inserted>([&](card_inserted const& m){
account=m.account; pin="";
interface.send(display_enter_pin());
state=&atm::getting_pin; });
}
void getting_pin(){
incoming.wait()
.handle<digit_pressed>([&](digit_pressed const& m){
pin+=m.digit;
if(pin.length()==4){ bank.send(verify_pin(account,pin,incoming)); state=&atm::verifying_pin; }})
.handle<clear_last_pressed>([&](...){ if(!pin.empty()) pin.resize(pin.leng()-1); })
.handle<cancel_pressed>([&](...){ state=&atm::done_processing; });
}
void run(){ state=&atm::waiting_for_card;
try{ for(;;) (this->*state)(); }
catch(messaging::close_queue const&){} }
};
要点:handle() 链式连接 wait(),不匹配的消息类型丢弃;每个状态一个成员函数,主循环反复运行当前状态函数 → 状态机。
4.4.3 experimental Future 与持续性(continuation)
std::experimental::future 的 then() 返回新的 future,原始 future 调用 then() 后 no valid;持续性参数是"就绪的 future"(更好处理异常);异常沿链传播.
spawn_async(4.17):
cpp
template<typename Func>
std::experimental::future<...> spawn_async(Func&& func){
std::experimental::promise<...> p;
auto res=p.get_future();
std::thread([p=std::move(p),f=std::decay_t<Func>(func)]() mutable{
try{ p.set_value_at_thread_exit(f()); }
catch(...){ p.set_exception_at_thread_exit(std::current_exception()); }
}).detach();
return res;
}
(标准规定支持持续性这些 API 用 set_value_at_thread_exit / set_exception_at_thread_exit。)
4.4.4 持续性连接:登录用例
- 同步(4.18) :
authenticate→request→update,try/catch。 - 异步阻塞(4.19) :
async( std::launch::async, [=]{ ... }),仍把显示放线程内阻塞。 - 持续性(4.20) :链式
spawn_async(...).then(...).then(...),每步异步不阻塞线程。 - 全异步(4.21) :
async_authenticate返回 future;future_unwrapping允许then()返回 future 从而继续链。
4.4.5 等待多个 future
- 低效
async(4.22):循环for(auto& f:futures) v.push(f.get())逐个唤醒(伪并发)。 when_all(4.23) :when_all(begin,end).then( cb )等待所有 future 就绪后一次性返回新 future。无反复轮询/上下文切换。
4.4.6 when_any
when_any返回when_any_result,含futures(集合)和index(哪个先就绪)。- 找到匹配:设置 shared promise final→
set_value(process_found_value); - 未找到:remains → 剔除该 index 后递归再
when_any等下一个;空则set_exception(runtime_error("Not found"))。
4.4.7-8 锁存器(latch)与栅栏(barrier)、flex_barrier
- latch :一次性倒数,降为0就绪并保持;不care哪个线程递减(同一线程可多次)。
count_down()、wait()、count_down_and_wait()。 - barrier :线程组内**所有成员到达才通过,可重用。
arrive_and_wait()两个阶段 用。 - flex_barrier:构造可带一个完成函数,在所有线程到达时由任一线程执行,可返回下一周期到达数量(-1=不变)。把"数据分块/输出"封装进完成函数,主循环只剩并行,代码简化。
5. 一句话记忆
- 事件等待:条件变量 优于忙轮询;sleep 可用但要选对 sleep 时长。
- 取结果:future / promise / packaged_task / shared_future 四种手段。
- 限时:
_for(相对)+_until(绝对);wait_until 配steady_clock才能限总时长。 - 写更简单:纯函数 FP (
async递归排序)、CSP 消息/状态机、experimental 的then/when_all/when_any、以及 latch/barrier/flex_barrier。
目标:用"同步操作"做高层抽象,让并发代码更不易出错、更易读,而非都被你亲手拼锁。
本归纳基于该站点 4.0--4.4(含代码)整理;实验类 API(when_all/when_any/latch/barrier/flex_barrier/experimental::future 持续段)属并发技术规格 std::experimental,标准仍收敛于 C++17+。