承接上一篇学习的std::thread这篇搞定多线程数据共享各种类互斥锁RAII锁管理1什么是数据竞争1数据竞争多个线程同时读写同一个共享全局 / 堆变量且没有任何同步保护就会产生数据竞争结果未定义、数值错乱。示例无锁错误代码#include iostream #include thread using namespace std; int g_cnt 0; void add() { for (int i 0; i 1000000; i) { g_cnt; } } int main() { thread t1(add); thread t2(add); t1.join(); t2.join(); // 理论应该 2000000实际永远小于这个数 cout g_cnt endl; return 0; }原因g_cnt不是原子操作拆成三步读内存 → 寄存器自增 → 写回内存多线程穿插执行就会覆盖丢失。2临界区和互斥锁临界区访问共享数据的代码片段互斥锁 mutex保证同一时刻只有一个线程进入临界区其他线程阻塞等待。2C11四种常见互斥锁分类锁类型特点适用场景std::mutex普通互斥锁、不可递归、非定时常规业务临界区最常用std::recursive_mutex可递归加锁同一线程可多次 lock递归函数、类成员函数嵌套加锁std::timed_mutex普通锁 超时等待不想死等等待一段时间拿不到锁就放弃std::recursive_timed_mutex可递归 超时递归且需要超时控制一个线程 lock 成功后其他线程 lock 阻塞、try_lock 返回 false锁没解锁就销毁 / 持有锁线程直接退出 → 行为未定义3std::mutex基础用法1核心接口void lock(); // 加锁拿不到就阻塞 void unlock(); // 解锁 bool try_lock(); // 尝试加锁拿到返回true拿不到立刻返回false不阻塞2基础加锁解锁示例#include iostream #include thread #include mutex using namespace std; int g_cnt 0; mutex mtx; void add() { for (int i 0; i 1000000; i) { mtx.lock(); g_cnt; mtx.unlock(); } } int main() { thread t1(add); thread t2(add); t1.join(); t2.join(); cout g_cnt endl; // 正常 2000000 return 0; }3手写lock/unlock的致命问题中途抛异常unlock 没执行 → 死锁代码分支多容易漏写 unlock可读性差成对维护麻烦所以必须用RAII 锁管理lock_guard /unique_lock4std::lock_guard简洁RAII自动锁1原理RAII 思想构造时自动 lock离开作用域析构时自动 unlock不用手动写 lock /unlock异常也能自动解锁2构造方式// 1. 常规加锁构造立刻lock explicit lock_guard(mutex_type m); // 2. 接管已经加好的锁adopt_lock lock_guard(mutex_type m, adopt_lock_t tag);3基础使用void add() { for (int i 0; i 1000000; i) { lock_guardmutex lg(mtx); // 临界区 g_cnt; // 离开作用域 lg 析构自动 unlock } }4adopt_lock场景已经手动 lock 了交给 lock_guard 接管析构自动解锁void print_id(int id) { mtx.lock(); lock_guardmutex lck(mtx, adopt_lock); cout thread # id endl; // 不用手动unlock出作用域自动解 }5lock_guard限制不能拷贝、不能移动不能手动解锁、不能延迟加锁功能极简只适合简单固定临界区5std::recursive_mutex递归互斥锁1解决什么问题普通std::mutex同一线程重复 lock 直接死锁。recursive_mutex 允许同一个线程多次加锁解锁次数要和加锁次数匹配。2适用场景递归函数内部加锁类多个成员函数都要加锁互相调用嵌套3示例recursive_mutex rmtx; void funcA() { rmtx.lock(); cout funcA endl; funcB(); rmtx.unlock(); } void funcB() { rmtx.lock(); // 同一线程递归加锁不会阻塞 cout funcB endl; rmtx.unlock(); }建议尽量少用递归锁能重构代码就重构递归锁性能略差、容易隐藏逻辑问题。6std::timed_mutex带超时互斥锁1新增接口// 尝试加锁阻塞指定时长超时拿不到返回false bool try_lock_for(chrono::duration); // 阻塞到某个时间点超时返回false bool try_lock_until(chrono::time_point);2经典事例每隔 200ms 打印-最多等 1 秒拿锁#include iostream #include thread #include mutex #include chrono using namespace std; timed_mutex mtx; void fireworks(int i) { // 最多等待1秒拿不到就循环打 - while (!mtx.try_lock_for(chrono::milliseconds(1000))) { cout -; } // 拿到锁 cout i; this_thread::sleep_for(chrono::milliseconds(5000)); cout *\n; mtx.unlock(); } int main() { thread threads[2]; for (int i 0; i 2; i) threads[i] thread(fireworks, i); for (auto th : threads) th.join(); return 0; }7std::unique_lock功能最强的RAII锁1为什么有了lock_guard还要unique_locklock_guard 太死板unique_lock 支持延迟加锁尝试加锁超时加锁手动 lock/unlock支持移动语义可以配合条件变量condition_variable必须用 unique_lock2七种构造方式构造方式含义无参空锁不绑定任何 mutex传 mutex构造直接加锁try_to_lock构造尝试加锁不阻塞defer_lock延迟加锁构造不加后面手动 lockadopt_lock接管已经加好的锁时间段try_lock_for 超时等待时间点try_lock_until 超时等待3常用示例1延迟加锁defer_lockmutex mtx; void test() { unique_lockmutex lk(mtx, defer_lock); // 做一些无关操作... lk.lock(); // 手动加锁 // 临界区 lk.unlock(); // 手动解锁 // 还可以再加锁 lk.lock(); }2支持移动不支持拷贝unique_lockmutex lk1(mtx); unique_lockmutex lk2 move(lk1); // 移动可以 // unique_lockmutex lk3 lk1; // 拷贝禁用3配合条件变量使用后面讲 condition_variable 会用到条件变量 wait 只能接收 unique_lock不能用 lock_guard。4lock_guard VS unique_lock简单临界区、不需要手动解锁、不需要条件变量 →lock_guard轻量需要延迟加锁、手动解锁、超时、移动、条件变量 →unique_lock8多锁同时加锁std::lock/std::try_lock1std::lock模版函数一次性锁住多个互斥锁避免死锁内部会自动处理加锁顺序若部分锁住、部分没锁住会先释放已锁住的再阻塞等待全部拿到。mutex m1, m2; void taskA() { lock(m1, m2); // 同时锁两个不会死锁 // 临界区 m1.unlock(); m2.unlock(); } void taskB() { lock(m2, m1); // 颠倒顺序也没事std::lock内部规避死锁 }2std::try_lock尝试一次性锁多个全部成功返回-1某个失败返回失败的下标并释放已拿到的锁9thread传参为什么必须用std::reftemplate class _Fn, class... _Args, enable_if_t!is_same_v_Remove_cvref_t_Fn, thread, int 0 _NODISCARD_CTOR explicit thread(_Fn _Fx, _Args ..._Ax) { _Start(_STD forward_Fn(_Fx), _STD forward_Args(_Ax)...); } template class _Fn, class... _Args void _Start(_Fn _Fx, _Args ..._Ax) { // 从下⾯可以看到线程要调⽤系统库的线程最终还是要把参数包打包成⼀个结构体对象再传给线程所以线程中拿到的参数包值是我们传的参数包值的拷⻉所以要⽤ref才传参才能解决问题 using _Tuple tupledecay_t_Fn, decay_t_Args...; auto _Decay_copied _STD make_unique_Tuple(_STD forward_Fn(_Fx), _STD forward_Args(_Ax)...); constexpr auto _Invoker_proc _Get_invoke_Tuple(make_index_sequence1 sizeof...(_Args){}); // pointer or reference to potentially throwing function passed to // extern C function under -EHc. Undefined behavior may occur // if this function throws an exception. (/Wall) _Thr._Hnd reinterpret_castvoid *(_CSTD _beginthreadex(nullptr, 0, _Invoker_proc, _Decay_copied.get(), 0, _Thr._Id)); if (_Thr._Hnd) { // ownership transferred to the thread (void)_Decay_copied.release(); } else { // failed to start thread _Thr._Id 0; _Throw_Cpp_error(_RESOURCE_UNAVAILABLE_TRY_AGAIN); } }线程底层会把所有参数打包成 tuple 做值拷贝传给系统线程入口。如果你直接传普通变量线程里拿到的是拷贝副本修改不影响外部。想要真正传引用必须用std::ref()/std::cref()包装让 tuple 推导为引用类型。示例标准写法void func(int x, mutex mtx) { lock_guardmutex lg(mtx); x; } int main() { int x 0; mutex mtx; // 必须ref否则编译报错 / 传值副本 thread t(func, ref(x), ref(mtx)); t.join(); return 0; }替代方案用 lambda 捕获引用不用传参更优雅thread t([x, mtx](){ lock_guardmutex lg(mtx); x; });10总结多线程共享变量必有数据竞争必须用互斥锁保护临界区std::mutex基础锁不可递归recursive_mutex支持同线程多次加锁timed_mutex带超时等待避免无限阻塞lock_guard极简 RAII自动加解锁性能好功能单一unique_lock功能最全支持延迟 / 尝试 / 手动加解锁、移动、适配条件变量std::lock/try_lock一次性多锁解决死锁问题线程传引用必须std::ref或 lambda 捕获引用更省事。