Chapter 4 并发同步操作 · 归纳总结

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(&parallel_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. 一句话记忆

  1. 事件等待:条件变量 优于忙轮询;sleep 可用但要选对 sleep 时长。
  2. 取结果:future / promise / packaged_task / shared_future 四种手段。
  3. 限时:_for(相对)+_until(绝对);wait_until 配 steady_clock 才能限总时长。
  4. 写更简单:纯函数 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+。

相关推荐
这个DBA有点耶16 小时前
ELT 工具选型完整指南:从 ETL 差异分析到工程落地
数据库·架构·代码规范
这个DBA有点耶18 小时前
分布式事务深度:TCC空回滚、悬挂与Saga补偿风暴的根因与解法
数据库·mysql·代码规范
这个DBA有点耶3 天前
为什么UPDATE比SELECT危险得多?锁范围等于扫描行数
数据库·架构·代码规范
沈麽鬼3 天前
纸上代码
代码规范
深圳老胡5 天前
STM32CubeMX 生成 CMake 工程,用 VSCode 编译与调试的完整流程
笔记·stm32·单片机·代码规范
星星泡饭吃5 天前
【信息安全】越权防护设计评审指南
代码规范
GlobalSign数字证书5 天前
申请 EV 代码签名证书需要准备哪些材料?
代码规范
深圳老胡5 天前
STM32F4 OTA 升级双 App 方案设计与实现
笔记·stm32·单片机·代码规范
深圳老胡5 天前
STM32F4 OTA升级:单App与双App方案优缺点对比
笔记·stm32·单片机·代码规范
这个DBA有点耶6 天前
Change Buffer深入:二级索引写入的隐形加速器与它的代价
数据库·mysql·代码规范