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::futurethen() 返回新的 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+。

相关推荐
Asize4 小时前
ESLint 到底在帮我们守住什么?从规则配置到工程实践
代码规范·eslint
嘟嘟07174 小时前
ESLint 入门:从 npm run lint 到 --fix 与规则级别一次讲清
javascript·代码规范·eslint
不好听6134 小时前
ESLint 从零到落地:把"代码规范"变成机器的强制检查
代码规范
用户9385156350717 小时前
ESLint 代码规范完全指南——从 AST 原理到 flat config 逐行解析
javascript·后端·代码规范
梦梦代码精2 天前
连锁品牌数字化:从门店扩张到用户资产运营的技术底座
大数据·人工智能·低代码·docker·开源·代码规范
嘟嘟07172 天前
用单例模式管理弹窗:从一段原生 JS 理解 Singlet
前端·javascript·代码规范
Patrick_Wilson5 天前
代码重构中的蚕食方式是什么
程序员·架构·代码规范
文心快码BaiduComate7 天前
文心快码荣获“中国优秀软件产品”,实力再获国家级认可
算法·代码规范
paopaokaka_luck8 天前
基于springboot3+vue3的企业考勤管理系统(部门树递归、Echarts图形化分析)
开发语言·spring boot·学习·echarts·mybatis·需求分析·代码规范