十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

c++并发--同步

c++并发--同步 1.std::condition_variable条件变量1.1.核心作用条件变量用于线程间等待某个条件成立配合 std::mutex 使用解决忙等问题。1.2.为什么需要它不用条件变量的经典错误写法// 错误忙等待浪费 CPUwhile(!ready){/* 空转 */}// 错误sleep 轮询响应慢且仍浪费 CPUwhile(!ready){std::this_thread::sleep_for(10ms);}1.3.三个关键操作std::mutex mtx;std::condition_variable cv;bool readyfalse;// 等待方voidwaiter(){std::unique_lockstd::mutexlock(mtx);// wait 会做两件事// 1. 原子地释放锁并阻塞防止错过 notify// 2. 被唤醒后重新获取锁并检查谓词cv.wait(lock,[]{returnready;});// 带谓词的版本推荐// 等价于// while (!ready) cv.wait(lock);std::cout条件满足继续执行\n;}// 通知方voidnotifier(){{std::lock_guardstd::mutexlock(mtx);readytrue;// 必须先改状态再通知}// 先解锁再 notify减少被唤醒线程等待锁的时间cv.notify_one();// 唤醒一个等待线程// cv.notify_all(); // 唤醒所有等待线程}为什么必须配合unique_lockwait需要在阻塞时释放锁被唤醒时重新加锁——锁的所有权需要可转移unique_lock满足lock_guard不行。这也是wait接收unique_lock而非mutex的原因。经典坑点虚假唤醒spurious wakeupwait可能在没有notify的情况下返回所以必须用while循环或带谓词版本检查条件。先改状态再通知否则可能通知后、等待方还没进入wait导致永久阻塞进入wait后不再发通知了。wait返回时不保证条件仍成立被唤醒到重新拿到锁之间其他线程可能又改了状态。不过拿到锁后会进行谓词检测可以保证此时谓词检测不通过释放锁再次等待。典型应用生产者-消费者队列templatetypename Tclass BlockingQueue{std::mutex mtx;std::condition_variable cv_not_empty,cv_not_full;std::queueTq;size_tcapacity;public:voidpush(T val){std::unique_lockstd::mutexlock(mtx);cv_not_full.wait(lock,[]{returnq.size()capacity;});q.push(std::move(val));cv_not_empty.notify_one();}Tpop(){std::unique_lockstd::mutexlock(mtx);cv_not_empty.wait(lock,[]{return!q.empty();});T valstd::move(q.front());q.pop();cv_not_full.notify_one();returnval;}};1.3.1.条件等待细节带谓词的cv.wait(lock, pred)的标准语义等价于// 标准库大致实现templatetypename Lock,typename Predvoidwait(Locklock,Pred pred){while(!pred()){// 注意pred 的调用发生在持有锁期间wait(lock);// 释放锁 阻塞原子操作被唤醒后重新加锁}}完整流程拆解进入时还没睡眠过加锁 → 检查pred()├─ true → 直接通过不睡眠 └─ false → 释放锁原子地进入阻塞被唤醒后从阻塞返回 → 重新获得锁 → 检查pred()├─ true → 返回继续执行业务逻辑 └─ false → 再次释放锁重新进入阻塞2.std::condition_variable_any与condition_variable的区别std::condition_variable只能与std::mutex配合使用而condition_variable_any可以与任何满足基本锁要求的互斥类型一起工作BasicLockable只需有lock()和unlock()。std::condition_variable_any cv_any;std::shared_mutex smtx;// shared_mutex 不是普通 mutexvoidreader(){std::shared_locklock(smtx);// 读锁cv_any.wait(lock,[]{returndata_ready;});}voidwriter(){{std::unique_locklock(smtx);data_readytrue;}cv_any.notify_all();}代价condition_variable_any是通用实现性能略低于专门优化的std::condition_variable。除非确实需要非标准互斥类型如shared_mutex、用户自定义锁、甚至跨进程的锁否则优先用condition_variable。3.std::promise承诺3.1.核心模型promise是一次性单写者通道某线程通过set_value / set_exception写入结果结果自动存到关联的shared state共享状态中供未来的future读取。std::promiseintp;std::futureintfp.get_future();// 必须在 set_value 之前获取std::threadt([p]{std::this_thread::sleep_for(1s);p.set_value(42);// 写入结果此后 future 可读到});std::coutf.get();// 阻塞直到结果就绪t.join();set_value之后set_value只能调用一次第二次调用抛std::future_error。它是线程安全的——可与future::get并发执行。异常传递std::threadt([p]{try{throw std::runtime_error(出错了);}catch(...){p.set_exception(std::current_exception());// 把异常存进共享状态}});try{f.get();// 会重新抛出该异常}catch(conststd::exceptione){std::coute.what();}使用场景当你自己创建线程并希望它向调用方返回结果/异常时promise是最底层的原语async和packaged_task都是基于它构建的。4.std::future期物核心模型future是共享状态的读端句柄代表一个未来才会有的值。三种就绪方式promise::set_value赋值packaged_task执行完毕async任务完成关键成员函数std::futureintf...;intvf.get();// 阻塞直到就绪只能调用一次移动语义取出结果f.wait();// 仅阻塞等待不取结果f.wait_for(100ms);// 限时等待返回 future_statusf.wait_until(tp);f.valid();// 是否关联了共享状态f.share();// 转为 shared_future调用后本对象失效future_statusautostatusf.wait_for(500ms);switch(status){casestd::future_status::ready:/* 就绪 */break;casestd::future_status::timeout:/* 超时 */break;casestd::future_status::deferred:/* 任务延迟未启动launch::deferred*/break;}关键限制future是一次性、独占的get()只能调用一次第二次会抛future_error因为结果是被move出来的。这也是shared_future存在的理由。析构行为重要陷阱如果future关联的是std::async启动的任务且未get/waitfuture析构时会阻塞直到任务完成。如果关联的是deferred任务析构不会执行该任务任务被丢弃。5.std::shared_future与future的区别shared_future允许多个线程多次读取同一个结果。std::promiseintp;std::shared_futureintsfp.get_future().share();// 或 p.get_future() 隐式转换// 多个线程可同时 get()std::threadt1([sf]{std::coutsf.get();});std::threadt2([sf]{std::coutsf.get();});// OK可复制特性futureshared_future复制不可复制只可移动可复制get()次数仅一次任意多次并发安全典型用途一对一传递结果广播结果给多个等待者使用场景一次计算、多处消费例如// 主线程加载配置多个工作线程等待配置就绪std::shared_futureConfigconfig_readystd::async(std::launch::async,load_config).share();std::thread workers[4];for(autow:workers)wstd::thread([config_ready]{process(config_ready.get());});注意shared_future本身对象不是线程安全的复制/析构需外部同步但并发调用get()是安全的。6.std::packaged_task核心模型把可调用对象 结果通道打包在一起包装后的函数对象被调用时返回值或异常自动存入关联的 shared state。std::packaged_taskint(int,int)task([](inta,intb){returnab;});std::futureintftask.get_future();task(3,4);// 在任意线程调用不一定在创建它的线程std::coutf.get();// 7关键用途线程池packaged_task是构建任务队列/线程池的标准方式——调用方不关心任务在哪个线程执行只通过future取结果class ThreadPool{std::vectorstd::threadworkers;std::queuestd::functionvoid()tasks;std::mutex mtx;std::condition_variable cv;bool stopfalse;public:ThreadPool(size_tn){for(size_ti0;in;i)workers.emplace_back([this]{while(true){std::functionvoid()task;{std::unique_locklk(mtx);cv.wait(lk,[]{returnstop||!tasks.empty();});if(stoptasks.empty())return;taskstd::move(tasks.front());tasks.pop();}task();}});}templatetypename F,typename...Argsautosubmit(Ff,Args...args)-std::futurestd::invoke_result_tF,Args...{using Rstd::invoke_result_tF,Args...;autotaskstd::make_sharedstd::packaged_taskR()(std::bind(std::forwardF(f),std::forwardArgs(args)...));std::futureRfuttask-get_future();{std::lock_guardlk(mtx);tasks.emplace([task]{(*task)();});}cv.notify_one();returnfut;}~ThreadPool(){{std::lock_guardlk(mtx);stoptrue;}cv.notify_all();for(autow:workers)w.join();}};// 使用ThreadPoolpool(4);autofutpool.submit([](intx){returnx*x;},5);std::coutfut.get();// 256.1.逐行拆解 submit 的实现6.1.1.模板签名部分万能引用forwarding referencetemplatetypename F,typename...Argsautosubmit(Ff,Args...args)-std::futurestd::invoke_result_tF,Args...F f中F是推导出来的模板参数所以F是万能引用而非右值引用调用方式F推导为f的类型submit([](int x){...}, 5)lambda 类型值lambda 的左值引用submit(std::move(func_obj), 5)同类型右值引用submit(some_func, 5)函数指针类型指针的左值引用配合后面的std::forward实现完美转发左值保持左值、右值保持右值lambda既可以拷贝也可以移动进来。std::invoke_result_tF, Args...—— 推导返回值类型autosubmit(...)-std::futurestd::invoke_result_tF,Args...// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^// 用 F 调用 Args... 会返回什么类型C17引入等价于typename std::invoke_resultF, Args...::type。6.1.2.打包任务using Rstd::invoke_result_tF,Args...;autotaskstd::make_sharedstd::packaged_taskR()(std::bind(std::forwardF(f),std::forwardArgs(args)...));std::packaged_taskR()—— 零参可调用的打包器packaged_task的模板参数是函数签名。R()表示无参数、返回R。std::bind —— 参数绑定 值类别转发std::bind(std::forwardF(f),std::forwardArgs(args)...)bind会拷贝或移动所有实参到内部存储生成一个新的可调用对象。6.1.3.为什么用 std::make_shared 而不是直接放 packaged_taskstd::queuestd::functionvoid()tasks;std::functionvoid()要求可调用对象可拷贝。而std::packaged_task是只可移动、不可拷贝的std::bind绑定出lambda也可能因捕获移动-only类型而不可拷贝。解决方案用shared_ptr做一层间接——shared_ptr本身可拷贝拷贝的只是控制块引用计数autotaskstd::make_sharedstd::packaged_taskR()(std::bind(std::forwardF(f),std::forwardArgs(args)...));// 队列里存的 lambda 捕获 shared_ptr可拷贝tasks.emplace([task]{(*task)();});生命周期问题也随之解决submit返回后局部task销毁但队列中的lambda还持有一份引用任务对象活到执行完毕。若没有这层shared_ptr任务对象在submit结束时就销毁了。6.1.4.获取 futurestd::futureRfuttask-get_future();future只可移动return fut;触发移动构造/移动返回值优化把读取端交还给调用者。6.1.5.入队与通知{std::lock_guardlk(mtx);tasks.emplace([task]{(*task)();});}cv.notify_one();先加锁放入释放锁再通知。可以包装通知唤醒等待者时等待者可以立即获得锁。唤醒一个是因为一次只放入一个任务。唤醒所有会导致其余唤醒者无意义唤醒。tasks.emplace(...) vs tasks.push(...)直接构造lambda进队列相比lambda的临时对象构造移动是顺手的小优化。7.std::async异步运行三种启动策略autof1std::async(func);// 默认实现决定可能 async 或 deferredautof2std::async(std::launch::async,func);// 强制新线程立即执行autof3std::async(std::launch::deferred,func);// 延迟调用 get()/wait() 时才在当前线程执行autof4std::async(std::launch::async|std::launch::deferred,func);// 任选其一各策略语义策略执行时机线程特点async调用即启动新线程真正并行deferred首次get()/wait()调用 get 的线程惰性求值可能永远不执行默认未指定未指定不可移植不要依赖注意deferred的陷阱autofstd::async(std::launch::deferred,[]{std::cout任务\n;});// 如果不调用 f.get()任务永远不会执行f.get();// 此时才在当前线程执行future析构阻塞陷阱:voidbad(){autofstd::async(std::launch::async,[]{std::this_thread::sleep_for(10s);});// f 离开作用域析构时会阻塞 ~10 秒}// 阻塞点异常传递:autofstd::async([]{throw std::runtime_error(失败);});try{f.get();}catch(conststd::exceptione){/* 捕获到任务中的异常 */}async vs thread的选择:std::threadstd::async返回值无需自行用 promise有 future异常处理线程内未捕获异常 → terminate异常经 future 传递线程创建手动管理实现可复用线程如线程池适用长生命周期、手动精细控制想要个结果的临时任务8.整体关系图┌─────────────────────────────────────────┐ │ 共享状态(shared state)│ │ 由实现管理的引用计数对象 │ └──────▲──────────────▲──────────────▲─────┘ │ │ │ 写入端三选一 │ │ │ 读取端 ┌───────────────────────┼──────────────┼──────────────┼──────────────┐ │ │ │ │ │ std::promise std::packaged_task std::async std::future std::shared_futureset_value()包装可调用对象 启动策略独占读取 共享读取set_exception()调用时自动写结果 自动绑定get()一次get()多次 │ │ │ │ └───────────────────────┴──────────────┴──────────────┘ 底层机制promise 是地基packaged_task 和 async 内部都基于它9.如何选择需求推荐组件线程间等待条件condition_variablemutex需要与 shared_mutex 等非常规锁配合等待condition_variable_any提交任务到线程池并取结果packaged_task 手写队列快速启动异步任务取结果std::async手动创建线程传递结果/异常std::promise一个结果多个消费者std::shared_future一对一传递结果std::future经验法则能用高层抽象就不用底层——async优先于手写thread promisepackaged_task优先于在线程函数里手动set_value带谓词的cv.wait(lock, pred)优先于裸while循环。
返回列表