大纲 C++ 并发编程 互斥量 互斥量的概念 C++ 中的互斥量(std::mutex)是一种用于保护共享数据免受并发访问影响的同步原语。它的核心机制是 “独占锁定”:同一时刻,只有一个线程能成功锁定(lock)互斥量并进入临界区执行代码,其他试图进入的线程必须等待,直到持有锁的线程调用解锁(unlock)释放所有权。这种机制有效防止了数据竞争(Data Race)和未定义行为。C++ 11 标准库将互斥量纳入了线程支持库,并提供了 std::lock_guard、std::unique_lock 等 RAII 包装器,用于自动管理锁的获取与释放,避免因异常或忘记解锁而导致的死锁问题。
互斥量的使用 lock()、unlock() 互斥量(std::mutex)的基本用法遵循显式的加锁与解锁配对原则:线程必须先调用 lock() 获取互斥量,然后安全地操作共享数据,最后调用 unlock() 释放所有权。
特别注意
互斥量(std::mutex)的 lock() 和 unlock() 必须严格成对使用,每次 lock() 对应一次 unlock(),既不能多调用一次 unlock(),也不能少调用一次,否则会导致未定义行为或线程死锁等严重问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #include <atomic> #include <iostream> #include <list> #include <mutex> #include <thread> class MyClass {public : void inMsgRecvQueue () { for (int i = 0 ; i < 1000 ; ++i) { msgRecvQueueMutex.lock (); msgRecvQueue.push_back (i); msgRecvQueueMutex.unlock (); std::this_thread::sleep_for (std::chrono::milliseconds (1 )); } stop = true ; } void outMsgRecvQueue () { while (true ) { int command = -1 ; msgRecvQueueMutex.lock (); if (stop && msgRecvQueue.empty ()) { msgRecvQueueMutex.unlock (); break ; } if (!msgRecvQueue.empty ()) { command = msgRecvQueue.front (); msgRecvQueue.pop_front (); } msgRecvQueueMutex.unlock (); if (command != -1 ) { std::cout << "已处理玩家命令: " << command << std::endl; } std::this_thread::sleep_for (std::chrono::milliseconds (1 )); } } private : std::list<int > msgRecvQueue; std::mutex msgRecvQueueMutex; std::atomic_bool stop{false }; }; int main () { std::cout << "main thread start." << std::endl; MyClass mc; std::thread t_write (&MyClass::inMsgRecvQueue, &mc) ; std::thread t_read (&MyClass::outMsgRecvQueue, &mc) ; t_write.join (); t_read.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 已处理玩家命令: 0 已处理玩家命令: 1 已处理玩家命令: 2 已处理玩家命令: 3 ...... 已处理玩家命令: 996 已处理玩家命令: 997 已处理玩家命令: 998 已处理玩家命令: 999
std::lock_guard 模板 C++ 11 标准库将互斥量纳入了线程支持库,并提供了 std::lock_guard、std::unique_lock 等 RAII 包装器,用于自动管理锁的获取与释放,避免因异常或忘记解锁而导致的死锁问题。值得一提的是,std::lock_guard 通过 RAII(资源获取即初始化)机制实现自动加锁和解锁:在构造时,它的构造函数会接收一个互斥量对象并立即调用该互斥量的 lock() 方法完成加锁;在析构时,它的析构函数会自动调用互斥量的 unlock() 方法完成解锁。这样,当 lock_guard 对象被创建时锁就被获取,当该对象离开作用域(无论是正常结束、return、break 还是抛出异常)时,析构函数会被自动触发,从而保证锁一定会被释放,避免了因忘记调用 unlock() 或异常导致死锁的问题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #include <atomic> #include <iostream> #include <list> #include <mutex> #include <thread> class MyClass {public : void inMsgRecvQueue () { for (int i = 0 ; i < 1000 ; ++i) { { std::lock_guard<std::mutex> lock (msgRecvQueueMutex) ; msgRecvQueue.push_back (i); } std::this_thread::sleep_for (std::chrono::milliseconds (1 )); } stop = true ; } void outMsgRecvQueue () { while (true ) { int command = -1 ; { std::lock_guard<std::mutex> lock (msgRecvQueueMutex) ; if (stop && msgRecvQueue.empty ()) { break ; } if (!msgRecvQueue.empty ()) { command = msgRecvQueue.front (); msgRecvQueue.pop_front (); } } if (command != -1 ) { std::cout << "已处理玩家命令: " << command << std::endl; } std::this_thread::sleep_for (std::chrono::milliseconds (1 )); } } private : std::list<int > msgRecvQueue; std::mutex msgRecvQueueMutex; std::atomic_bool stop{false }; }; int main () { std::cout << "main thread start." << std::endl; MyClass mc; std::thread t_write (&MyClass::inMsgRecvQueue, &mc) ; std::thread t_read (&MyClass::outMsgRecvQueue, &mc) ; t_write.join (); t_read.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 已处理玩家命令: 0 已处理玩家命令: 1 已处理玩家命令: 2 已处理玩家命令: 3 ...... 已处理玩家命令: 996 已处理玩家命令: 997 已处理玩家命令: 998 已处理玩家命令: 999
线程死锁 C++ 中的线程死锁是指两个或多个线程在执行过程中,因互相等待对方持有的资源(如互斥锁)而永远阻塞的状态。典型场景是线程 A 持有锁 1 并等待锁 2,而线程 B 持有锁 2 并等待锁 1,双方都无法继续推进。死锁的本质是资源竞争和循环依赖,会导致程序完全停滞,既不会崩溃也不会自动恢复。避免死锁的常见策略包括:按固定顺序获取锁、使用超时机制(如 try_lock())、采用分层锁或使用 RAII 管理锁的持有时间,从而打破循环等待条件。
死锁的四个必要条件 代码中的体现 互斥 std::mutex 保证同一时间只有一个线程能持有锁持有并等待 线程 1 持有 lock1 等待 lock2;线程 2 持有 lock2 等待 lock1 不可抢占 std::mutex 不能被强制释放,只能由持有者主动 unlock(解锁)循环等待 线程 1 → lock1 → lock2,线程 2 → lock2 → lock1,形成环路
触发线程死锁的案例 下面这段代码会触发线程死锁,因为两个线程以相反的顺序获取锁 :线程 1 先锁 lock1 再等待 lock2,线程 2 先锁 lock2 再等待 lock1,导致两个线程各自持有一个锁并等待对方释放另一个锁,形成循环等待,结果谁也无法继续执行,程序永久阻塞。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include <chrono> #include <iostream> #include <mutex> #include <thread> class MyClass {public : void func1 () { std::cout << "thread 1 wait to get locker 1" << std::endl; lock1.lock (); std::cout << "thread 1 already got locker 1" << std::endl; std::this_thread::sleep_for (std::chrono::milliseconds (1000 )); std::cout << "thread 1 wait to get locker 2" << std::endl; lock2.lock (); std::cout << "thread 1 already got locker 2" << std::endl; lock2.unlock (); std::cout << "thread 1 already released locker 2" << std::endl; lock1.unlock (); std::cout << "thread 1 already released locker 1" << std::endl; } void func2 () { std::cout << "thread 2 wait to get locker 2" << std::endl; lock2.lock (); std::cout << "thread 2 already got locker 2" << std::endl; std::this_thread::sleep_for (std::chrono::milliseconds (1000 )); std::cout << "thread 2 wait to get locker 1" << std::endl; lock1.lock (); std::cout << "thread 2 already got locker 1" << std::endl; lock1.unlock (); std::cout << "thread 2 already released locker 1" << std::endl; lock2.unlock (); std::cout << "thread 2 already released locker 2" << std::endl; } private : std::mutex lock1; std::mutex lock2; }; int main () { std::cout << "main thread start." << std::endl; MyClass mc; std::thread t1 (&MyClass::func1, &mc) ; std::thread t2 (&MyClass::func2, &mc) ; t1.join (); t2.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 main thread start. thread 1 wait to get locker 1 thread 1 already got locker 1 thread 2 wait to get locker 2 thread 2 already got locker 2 thread 1 wait to get locker 2 thread 2 wait to get locker 1 (程序卡住,永远不会打印后续的 "already got" 消息)
解决线程死锁的案例 解决线程死锁的方案
在 C++ 中,解决线程死锁的方案有以下几种:
(1) 破坏互斥条件
使用读写锁(std::shared_mutex)允许多个读 使用无锁编程(std::atomic) (2) 破坏持有并等待条件
std::lock() 一次性获取所有锁预先分配所有资源后再执行 (3) 破坏不可抢占条件
使用 std::timed_mutex + try_lock_for() 设置超时 超时后释放已持有锁,重试 (4) 破坏循环等待条件
所有线程以相同顺序获取锁(最常用) 为锁分配层级,按层级顺序锁定 (5) 辅助手段
最小化临界区代码 减少锁的数量,合并临界区 使用 std::lock_guard / std::unique_lock 自动释放锁 使用死锁检测工具(静态分析) 避免死锁的最常见方法 破坏的条件 复杂度 性能 适用场景 统一获取锁的顺序 循环等待 低 高 锁数量少,顺序明确 一次性获取所有锁(std::lock()) 循环等待 低 中 多锁场景,推荐优先使用 加入超时等待机制(std::timed_mutex + try_lock_for()) 持有并等待 高 低 实时系统,要求响应性
使用 std::lock () 一次性获取多个锁
在 C++ 中,std::lock() 能够一次性锁住两个或两个以上的互斥量(std::mutex),至少两个,一个不行。它的核心优势是从根本上避免了因锁的顺序不一致而导致的死锁风险 。工作原理是:如果其中某个互斥量没锁住,它就会一直等待,直到所有互斥量都锁住才会继续执行;关键是它保证了要么全部锁住,要么全部没锁住;如果只锁住了其中一个互斥量,而另一个互斥量没锁成功,它会立即把已经锁住的互斥量释放掉。
线程死锁解决案例一 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 #include <chrono> #include <iostream> #include <mutex> #include <thread> class MyClass {public : void func1 () { std::cout << "thread 1 wait to get locker 1" << std::endl; lock1.lock (); std::cout << "thread 1 already got locker 1" << std::endl; std::this_thread::sleep_for (std::chrono::milliseconds (1000 )); std::cout << "thread 1 wait to get locker 2" << std::endl; lock2.lock (); std::cout << "thread 1 already got locker 2" << std::endl; lock2.unlock (); std::cout << "thread 1 already released locker 2" << std::endl; lock1.unlock (); std::cout << "thread 1 already released locker 1" << std::endl; } void func2 () { std::cout << "thread 2 wait to get locker 1" << std::endl; lock1.lock (); std::cout << "thread 2 already got locker 1" << std::endl; std::this_thread::sleep_for (std::chrono::milliseconds (1000 )); std::cout << "thread 2 wait to get locker 2" << std::endl; lock2.lock (); std::cout << "thread 2 already got locker 2" << std::endl; lock2.unlock (); std::cout << "thread 2 already released locker 2" << std::endl; lock1.unlock (); std::cout << "thread 2 already released locker 1" << std::endl; } private : std::mutex lock1; std::mutex lock2; }; int main () { std::cout << "main thread start." << std::endl; MyClass mc; std::thread t1 (&MyClass::func1, &mc) ; std::thread t2 (&MyClass::func2, &mc) ; t1.join (); t2.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 main thread start. thread 1 wait to get locker 1 thread 1 already got locker 1 thread 2 wait to get locker 1 thread 1 wait to get locker 2 thread 1 already got locker 2 thread 1 already released locker 2 thread 1 already released locker 1 thread 2 already got locker 1 thread 2 wait to get locker 2 thread 2 already got locker 2 thread 2 already released locker 2 thread 2 already released locker 1 main thread end.
线程死锁解决案例二 特别注意
使用 std::lock() 锁住多个互斥量(std::mutex)时,如果不使用 std::lock_guard 或 std::unique_lock,则必须手动调用 std::mutex 的 unlock() 释放锁。强烈建议采用 RAII 方式管理锁,此时需要配合 std::adopt_lock 参数,这个参数的作用是告诉 std::lock_guard 或 std::unique_lock:互斥量已经被 std::lock() 锁住了,不要在尝试构造时加锁,只需在析构时自动解锁即可。这样做不仅能在作用域结束时自动释放锁,避免因忘记解锁而引发的资源泄漏,还能在临界区抛出异常时保证锁被正确释放,从而大幅提升代码的安全性和健壮性,同时减少手动管理带来的出错风险。
线程死锁解决方案:使用 std::lock() 一次性获取所有锁 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 #include <chrono> #include <iostream> #include <mutex> #include <thread> class MyClass {public : void func1 () { std::cout << "thread 1 wait to get locker 1 and locker 2" << std::endl; std::lock (lock1, lock2); std::cout << "thread 1 already got locker 1 and locker 2" << std::endl; std::lock_guard<std::mutex> g1 (lock1, std::adopt_lock) ; std::lock_guard<std::mutex> g2 (lock2, std::adopt_lock) ; std::this_thread::sleep_for (std::chrono::milliseconds (1000 )); std::cout << "thread 1 already released locker 1 and locker 2" << std::endl; } void func2 () { std::cout << "thread 2 wait to get locker 1 and locker 2" << std::endl; std::lock (lock1, lock2); std::cout << "thread 2 already got locker 1 and locker 2" << std::endl; std::lock_guard<std::mutex> g1 (lock1, std::adopt_lock) ; std::lock_guard<std::mutex> g2 (lock2, std::adopt_lock) ; std::this_thread::sleep_for (std::chrono::milliseconds (1000 )); std::cout << "thread 2 already released locker 1 and locker 2" << std::endl; } private : std::mutex lock1; std::mutex lock2; }; int main () { std::cout << "main thread start." << std::endl; MyClass mc; std::thread t1 (&MyClass::func1, &mc) ; std::thread t2 (&MyClass::func2, &mc) ; t1.join (); t2.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 main thread start. thread 1 wait to get locker 1 and locker 2 thread 1 already got locker 1 and locker 2 thread 2 wait to get locker 1 and locker 2 thread 1 already released locker 1 and locker 2 thread 2 already got locker 1 and locker 2 thread 2 already released locker 1 and locker 2 main thread end.
unique_lock unique_lock 是一个通用互斥锁(std::mutex)包装器,通过 RAII(资源获取即初始化)机制自动管理锁的获取与释放,从根本上避免因异常或返回导致的死锁。与简单的 lock_guard 相比,它提供了延迟加锁、尝试加锁、提前解锁和所有权转移等灵活特性,但这也带来稍大的内存与性能开销。其最关键的专属用途是必须配合 condition_variable 使用,因为 wait() 操作需要动态释放和重新获取锁。简言之,unique_lock 是复杂锁定场景与线程同步通信的核心工具,而在只需基础互斥保护的简单场景下,优先考虑轻量的 lock_guard 或 scoped_lock(C++ 17 开始支持)。
unique_lock 的核心作用 核心作用:RAII(资源获取即初始化)锁管理
unique_lock 的核心思想是利用对象的构造函数和析构函数,自动管理互斥量的加锁与解锁。构造时:调用 mutex.lock()(或尝试加锁)。 析构时:自动调用 mutex.unlock()。 这带来了最直接的好处:即使代码在执行过程中抛出异常或提前 return,unique_lock 的析构函数也一定会被执行,从而彻底避免死锁(因为锁一定会被释放)。 相较于 lock_guard 的增强(独特优势)
虽然 lock_guard 也能做到 RAII,但 unique_lock 更加灵活。它弥补了 std::mutex 原生接口和 lock_guard 的不足,提供了以下关键特性: 延迟加锁(Deferred Locking):构造时可以不立即加锁,而是稍后在需要时手动调用 lock()。这在需要将锁的创建与加锁分离时非常有用。 尝试加锁(Try Locking):支持 try_lock(), try_lock_for(), try_lock_until(),允许线程在尝试获取锁失败时去做其他事情,而不是阻塞等待,提高了程序的响应性。 所有权转移(Move Semantics):unique_lock 是可移动的(但不可拷贝)。你可以将锁的所有权从一个对象转移给另一个对象,或者从函数中返回一个已加锁的 unique_lock,这极大地方便了在函数间传递锁的管控权。 提前解锁(Manual Unlock):在锁的作用域结束前,可以手动调用 unlock()。这在需要保护一段临界区极短的代码,且后续还有耗时操作时非常有用 —— 提前释放锁以减少锁持有的时间,提高并发度。 unique_lock 的构造参数 在 C++ 中,unique_lock 的构造函数最常用的参数主要有以下几种:
std::adopt_lock_t(接管锁)
假设当前线程已经成功获取了互斥量的锁(比如使用 std::lock()),unique_lock 在构造时不再尝试加锁,而是直接接管该锁的所有权,并在析构时自动解锁。 示例:std::unique_lock<std::mutex> lk(mtx, std::adopt_lock); std::defer_lock_t(延迟加锁)
unique_lock 在构造时不加锁,仅持有互斥量的引用。后续需要时再通过调用 lk.lock() 手动加锁。常用于将对象创建与加锁时机分离。示例:std::unique_lock<std::mutex> lk(mtx, std::defer_lock); std::try_to_lock_t(尝试加锁)
unique_lock 在构造时调用 mutex.try_lock() 尝试加锁。加锁结果可通过 lk.owns_lock() 判断,即使加锁失败也不会阻塞线程,允许程序做其他处理。示例:std::unique_lock<std::mutex> lk(mtx, std::try_to_lock); 超时时间(时间点或等待时长)
unique_lock 在构造时尝试加锁,但若无法立即获取,则阻塞等待直至超时。支持相对时长(std::chrono::duration)或绝对时间点(std::chrono::time_point)。示例:std::unique_lock<std::timed_mutex> lk(mtx, std::chrono::seconds(1));(等待 1 秒)std::unique_lock<std::timed_mutex> lk(mtx, std::chrono::steady_clock::now() + std::chrono::seconds(1)); 默认情况(无参数):
unique_lock 在构造时立即调用 mutex.lock(),阻塞当前线程直到获取锁为止。特别注意
使用 unique_lock 时需注意,除默认构造函数外,以上参数必须与互斥量类型(std::mutex)匹配(例如,超时参数要求互斥量支持 try_lock_for 或 try_lock_until,即 std::timed_mutex 等)。
unique_lock 的成员函数 unique_lock 的常用成员函数主要分为锁操作、状态查询和所有权管理三类:
锁操作(加锁 / 解锁)
lock():手动加锁。若互斥量已被其他线程持有,则阻塞等待,需在未持有锁时调用。try_lock():尝试加锁。立即返回,成功返回 true,失败返回 false,不阻塞线程。try_lock_for(duration):尝试加锁,等待指定的相对时长(需互斥量支持 try_lock_for(),如 std::timed_mutex)。try_lock_until(time_point):尝试加锁,等待直到指定的绝对时间点(需互斥量支持 try_lock_until)。unlock():手动解锁。通常用于提前释放锁,减少临界区持有时间,需确保当前线程持有锁。锁状态查询
owns_lock():返回 bool,表示当前是否持有锁。常用于 try_lock() 系列操作后判断是否成功获取锁。operator bool():与 owns_lock() 功能相同,允许在条件判断中直接使用对象(如 if (lk) { ... })。mutex():返回指向所管理互斥量的指针(不常用,一般用于代码调试)。所有权管理
release():释放所有权,即解除 unique_lock 与互斥量(std::mutex)的关联,但不解锁互斥量。返回互斥量指针,调用者需负责后续手动解锁 。常用于转移锁管理责任。移动语义(非成员函数风格):unique_lock 可移动(包括移动构造函数和移动赋值运算符),允许将锁的所有权从一个对象转移给另一个对象。 1 2 3 4 5 6 7 8 9 10 std::timed_mutex mtx; std::unique_lock<std::timed_mutex> lk (mtx, std::defer_lock) ; if (lk.try_lock_for (std::chrono::milliseconds (100 ))) { lk.unlock (); } else { }
特别注意
unique_lock 的 lock() 和 unlock() 必须成对使用,且调用前需确认当前锁状态,否则可能导致未定义行为(如重复加锁或解锁未持有的锁)。unique_lock 调用 release() 后,unique_lock 析构时不会再自动解锁,调用者需负责后续手动解锁,使用时需格外谨慎。unique_lock 在大部分场景下,开发者无需手动调用 unlock(),利用 RAII 自动析构即可;手动解锁主要用于优化性能(提前释放锁)。unique_lock 的所有权传递 在 C++ 中,unique_lock 遵循移动语义,其互斥量(std::mutex)的所有权可以转移,但不能复制 。这意味着一个 unique_lock 对象可以通过移动构造函数或移动赋值运算符将其对互斥量的管理权交给另一个 unique_lock 对象,转移后原 unique_lock 对象不再持有锁,从而避免多个 unique_lock 对象同时管理同一互斥量。这种设计既保证了资源管理的唯一性,又提供了在不同作用域或函数间传递锁所有权的灵活性。
特别注意
unique_lock 对象不能被复制:如果试图写 std::unique_lock<std::mutex> lock2 = lock;(拷贝),编译器会报错。unique_lock 所有权转移后,原 unique_lock 对象会变为空状态(不持有任何锁),其 owns_lock() 返回 false,析构时不会尝试解锁。unique_lock 所有权转移后,不能再对原 unique_lock 对象调用 lock() 和 unlock() 等方法,否则会导致未定义行为(程序崩溃、运行时抛出异常等)。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <iostream> #include <mutex> int main () { std::mutex mx; std::unique_lock<std::mutex> lock1 (mx) ; std::unique_lock<std::mutex> lock2 (std::move(lock1)) ; return 0 ; }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <iostream> #include <mutex> std::unique_lock<std::mutex> getLock (std::mutex &mx) { std::unique_lock<std::mutex> lock (mx) ; return lock; } int main () { std::mutex mx; std::unique_lock<std::mutex> lock = getLock (mx); return 0 ; }
unique_lock 的完整使用案例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 #include <atomic> #include <iostream> #include <list> #include <mutex> #include <thread> class MyClass {public : void inMsgRecvQueue () { for (int i = 0 ; i < 1000 ; ++i) { { std::unique_lock<std::mutex> lock (msgRecvQueueMutex) ; msgRecvQueue.push_back (i); } std::this_thread::sleep_for (std::chrono::milliseconds (1 )); } stop = true ; } void outMsgRecvQueue () { while (true ) { int command = -1 ; { std::unique_lock<std::mutex> lock (msgRecvQueueMutex) ; if (stop && msgRecvQueue.empty ()) { break ; } if (!msgRecvQueue.empty ()) { command = msgRecvQueue.front (); msgRecvQueue.pop_front (); } } if (command != -1 ) { std::cout << "已处理玩家命令: " << command << std::endl; } std::this_thread::sleep_for (std::chrono::milliseconds (1 )); } } private : std::list<int > msgRecvQueue; std::mutex msgRecvQueueMutex; std::atomic_bool stop{false }; }; int main () { std::cout << "main thread start." << std::endl; MyClass mc; std::thread t_write (&MyClass::inMsgRecvQueue, &mc) ; std::thread t_read (&MyClass::outMsgRecvQueue, &mc) ; t_write.join (); t_read.join (); std::cout << "main thread end." << std::endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 6 7 8 9 已处理玩家命令: 0 已处理玩家命令: 1 已处理玩家命令: 2 已处理玩家命令: 3 ...... 已处理玩家命令: 996 已处理玩家命令: 997 已处理玩家命令: 998 已处理玩家命令: 999
单例设计模式 在 C++ 中使用单例模式时,一般不用管单例对象的内存释放问题,也就是不需要编写自定义的析构函数。
单例设计模式实现 饿汉单例模式 饿汉单例模式是指还没有获取单例对象,单例对象就已经创建完成(初始化)了。值得一提的是,饿汉单例模式是线程安全的 ,因为单例对象在 main() 函数执行前就已经初始化完成(静态初始化),所以是线程安全的(因为对象已经存在,只是返回指针)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #include <iostream> #include <thread> using namespace std;class MyClass {private : MyClass () { cout << "MyClass()" << endl; } ~MyClass () { cout << "~MyClass()" << endl; } MyClass (const MyClass&) = delete ; MyClass& operator =(const MyClass&) = delete ; public : static MyClass* getInstance () { return m_instance; } private : static MyClass* m_instance; }; MyClass* MyClass::m_instance = new MyClass (); int main () { MyClass* mc = MyClass::getInstance (); MyClass* mc2 = MyClass::getInstance (); cout << "address: " << mc << endl; cout << "address: " << mc2 << endl; cout << "equals: " << (mc == mc2 ? "true" : "false" ) << endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 MyClass() address: 0x55a0fe5b4eb0 address: 0x55a0fe5b4eb0 equals: true
资源泄漏问题
上面饿汉单例模式的代码存在资源泄漏问题,不推荐使用(建议使用静态局部变量实现单例模式),原因如下: (1) 对象在堆上分配内存,但从未被 delete,内存不会被释放; (2) 由于析构函数是 private,即使开发者想手动 delete 也做不到; (3) 当程序退出时,对象的析构函数不会被调用,资源永远无法得到释放。 懒汉单例模式 懒汉单例模式是指在第一次被使用时才创建单例对象,而不是程序启动时就创建。特别注意,懒汉单例模式不一定是线程安全的,由具体的代码实现决定。 线程安全的懒汉单例模式有以下几种实现方式:
实现方式 是否线程安全 防止指令重排 推荐指数 静态局部变量 ✅(需要 C++ 11 及以上的编译器) ✅ ⭐⭐⭐⭐(最推荐,最简洁) mutex + atomic + DCL✅(兼容 C++ 11 以下的编译器,如 C++ 98 / C++ 03 ✅ ⭐⭐⭐(适合对底层控制有要求的场景)
C++ 基于静态局部变量实现懒汉单例模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 #include <iostream> #include <thread> using namespace std;class MyClass {private : MyClass () { cout << "MyClass()" << endl; } ~MyClass () { cout << "~MyClass()" << endl; } MyClass (const MyClass&) = delete ; MyClass& operator =(const MyClass&) = delete ; public : static MyClass* getInstance () { static MyClass instance; return &instance; } }; int main () { MyClass* mc = MyClass::getInstance (); MyClass* mc2 = MyClass::getInstance (); cout << "address: " << mc << endl; cout << "address: " << mc2 << endl; cout << "equals: " << (mc == mc2 ? "true" : "false" ) << endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 MyClass() address: 0x55b578666192 address: 0x55b578666192 equals: true ~MyClass()
C++ 基于 DCL(双重检查锁)实现懒汉单例模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 #include <atomic> #include <iostream> #include <mutex> #include <thread> class MyClass {private : MyClass () { std::cout << "MyClass()" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } MyClass (const MyClass&) = delete ; MyClass& operator =(const MyClass&) = delete ; public : static MyClass* getInstance () { MyClass* ptr = m_instance.load (std::memory_order_acquire); if (ptr == nullptr ) { std::lock_guard<std::mutex> lock (m_mutex) ; ptr = m_instance.load (std::memory_order_relaxed); if (ptr == nullptr ) { ptr = new MyClass (); m_instance.store (ptr, std::memory_order_release); } } return ptr; } static void destroyInstance () { std::lock_guard<std::mutex> lock (m_mutex) ; MyClass* ptr = m_instance.exchange (nullptr ); if (ptr != nullptr ) { delete ptr; } } private : static std::mutex m_mutex; static std::atomic<MyClass*> m_instance; }; std::mutex MyClass::m_mutex; std::atomic<MyClass*> MyClass::m_instance (nullptr ) ;int main () { MyClass* mc = MyClass::getInstance (); MyClass* mc2 = MyClass::getInstance (); std::cout << "address: " << mc << std::endl; std::cout << "address: " << mc2 << std::endl; std::cout << "equals: " << (mc == mc2 ? "true" : "false" ) << std::endl; MyClass::destroyInstance (); return 0 ; }
程序运行的结果如下:
1 2 3 4 5 MyClass ()address: 0x55ec9d5bbeb0 address: 0x55ec9d5bbeb0 equals: true ~MyClass ()
在写单例模式时,很多开发者喜欢提供一个类似 destroyInstance() 的方法来手动释放内存。开发者可能觉得自己在里面加了互斥锁(Mutex)和原子操作(Atomic)就已经万无一失了,但实际上,这在多线程环境下埋下了一个致命的隐患。
假设有以下并发场景:
(1) 线程 A 调用 getInstance(),通过了空指针检查,成功拿到了单例对象的指针 ptr。 (2) 线程 B 此时突然调用了 destroyInstance()。因为有锁保护,它成功将内部指针置空,并无情地执行了 delete ptr;。 (3) 线程 A 根本不知道单例对象已经被销毁。它兴高采烈地拿着手里的 ptr 去调用成员函数(例如 ptr->doSomething())。 错误原因分析:
错误结果:线程 A 访问了已经被释放内存的单例对象指针(野指针 / 悬空指针),引发未定义行为,导致程序直接崩溃。 核心矛盾:锁和原子操作只能保护单例 “指针本身” 的修改是安全的,但它们无法保护其他线程手中已经持有的、指向单例内存的存根。 最佳实践:在现代 C++(C++11 及以后)中,永远不要尝试手动销毁单例对象。最优雅、最安全的做法是使用静态局部变量来实现单例模式。 正常释放单例对象的内存:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 #include <atomic> #include <iostream> #include <mutex> #include <thread> class MyClass {private : MyClass () { std::cout << "MyClass()" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } MyClass (const MyClass&) = delete ; MyClass& operator =(const MyClass&) = delete ; public : static MyClass* getInstance () { MyClass* ptr = m_instance.load (std::memory_order_acquire); if (ptr == nullptr ) { std::lock_guard<std::mutex> lock (m_mutex) ; ptr = m_instance.load (std::memory_order_relaxed); if (ptr == nullptr ) { ptr = new MyClass (); static GcClass gc; m_instance.store (ptr, std::memory_order_release); } } return ptr; } class GcClass { public : GcClass () { } ~GcClass () { MyClass* ptr = MyClass::getInstance (); if (ptr == nullptr ) { delete ptr; ptr = nullptr ; } } }; private : static std::mutex m_mutex; static std::atomic<MyClass*> m_instance; }; std::mutex MyClass::m_mutex; std::atomic<MyClass*> MyClass::m_instance (nullptr ) ;int main () { MyClass* mc = MyClass::getInstance (); MyClass* mc2 = MyClass::getInstance (); std::cout << "address: " << mc << std::endl; std::cout << "address: " << mc2 << std::endl; std::cout << "equals: " << (mc == mc2 ? "true" : "false" ) << std::endl; return 0 ; }
程序运行的结果如下:
1 2 3 4 5 MyClass() address: 0x55c7c52d6eb0 address: 0x55c7c52d6eb0 equals: true ~MyClass()
共享数据问题分析 在多线程环境中,下面的单例设计模式代码存在严重的线程安全问题(共享数据问题)。这是因为在多线程环境下,下述代码的线程安全问题核心在于 getInstance() 中的 “检查 - 然后 - 操作” 竞态条件:当多个线程同时首次调用 getInstance() 并检测到 m_instance == nullptr 时,它们都会通过 new MyClass() 创建各自独立的实例,导致单例规则被破坏,并且由于析构函数私有,这些多余的对象永远无法被正确释放,造成内存泄漏;同时,new 操作本身涉及内存分配和构造两个步骤,若无内存屏障保护,其他线程可能看到一个未完全构造完成的对象,进而引发未定义行为。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 #include <iostream> #include <thread> class MyClass {private : MyClass () { std::cout << "MyClass()" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } MyClass (const MyClass&) = delete ; MyClass& operator =(const MyClass&) = delete ; public : static MyClass* getInstance () { if (m_instance == nullptr ) { m_instance = new MyClass (); } return m_instance; } private : static MyClass* m_instance; }; MyClass* MyClass::m_instance (nullptr ) ;void run () { MyClass* ptr = MyClass::getInstance (); std::cout << "address: " << ptr << std::endl; } int main () { std::thread t1 (run) ; std::thread t2 (run) ; std::thread t3 (run) ; t1.join (); t2.join (); t3.join (); return 0 ; }
程序运行的结果如下(可以发现 MyClass 类的构造函数被调用了多次,破坏了单例规则):
1 2 3 4 5 6 MyClass() address: 0x7f84e4000b70 MyClass() address: 0x7f84dc000b70 MyClass() address: 0x7f84ec000b70
共享数据问题解决 在多线程环境中,可以使用互斥量(mutex)+ 原子操作(atomic) + DCL(双重检查锁)来解决上面单例设计模式代码存在的线程安全问题(共享数据问题)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 #include <atomic> #include <iostream> #include <mutex> #include <thread> class MyClass {private : MyClass () { std::cout << "MyClass()" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } MyClass (const MyClass&) = delete ; MyClass& operator =(const MyClass&) = delete ; public : static MyClass* getInstance () { MyClass* ptr = m_instance.load (std::memory_order_acquire); if (ptr == nullptr ) { std::lock_guard<std::mutex> lock (m_mutex) ; ptr = m_instance.load (std::memory_order_relaxed); if (ptr == nullptr ) { ptr = new MyClass (); m_instance.store (ptr, std::memory_order_release); } } return ptr; } private : static std::mutex m_mutex; static std::atomic<MyClass*> m_instance; }; std::mutex MyClass::m_mutex; std::atomic<MyClass*> MyClass::m_instance (nullptr ) ;void run () { MyClass* ptr = MyClass::getInstance (); std::cout << "address: " << ptr << std::endl; } int main () { std::thread t1 (run) ; std::thread t2 (run) ; std::thread t3 (run) ; t1.join (); t2.join (); t3.join (); return 0 ; }
程序运行的结果如下(可以发现 MyClass 类的构造函数不会被调用多次,单例规则没有被破坏):
1 2 3 4 MyClass() address: 0x7f44e0000b70 address: 0x7f44e0000b70 address: 0x7f44e0000b70