C++ 并发编程:全面解析主流锁管理类
在 C++ 的并发世界里,管理共享资源就像是在一个繁忙的十字路口指挥交通。如果指挥不当,就会发生混乱甚至致命的“死锁”。C++ 标准库提供的各种锁管理工具,就是我们手中的“交通信号灯”,它们各自拥有独特的职能,帮助我们编写出安全、高效且优雅的多线程代码。
1. std::lock_guard
:忠诚的卫士
std::lock_guard
是最基础、最可靠的守卫。它就像一个忠诚的士兵,一旦被部署(构造),就会牢牢地守住阵地(锁定互斥量),直到任务完成(超出作用域)自动卸下职责。它从不偷懒,也从不犯错,无论程序是正常退出还是因异常而中断,它都确保锁被安全释放。
它的优势在于简单而纯粹:你只需要告诉它要守护哪个互斥量,剩下的它都会为你自动完成。在你的代码中,如果只需要在一个作用域内独占访问一个资源,lock_guard
永远是你的首选,因为它没有多余的开销,也不给你犯错的机会。
-
核心特性:
- 自动加锁与解锁:遵循 RAII 原则,生命周期与作用域绑定。
- 不可移动、不可拷贝:确保锁的所有权唯一。
- 无死锁避免:如果你需要锁定多个互斥量,必须手动确保所有线程都以相同的顺序加锁,否则可能发生死锁。
-
适用场景:
- 当你需要在一个函数或一个代码块中安全地锁定一个互斥量时。这是最常见的独占锁使用模式。
-
示例:
#include <iostream> #include <thread> #include <mutex> #include <vector>std::mutex mtx; int shared_data = 0;void safe_increment() {// 创建 lock_guard 对象,mtx 在这里被锁定std::lock_guard<std::mutex> lock(mtx);// 在此作用域内安全访问共享资源shared_data++;// 当 lock 超出作用域,mtx 会自动解锁 } // 这里会自动调用 lock.unlock()int main() {std::vector<std::thread> threads;for (int i = 0; i < 10; ++i) {threads.emplace_back(safe_increment);}for (auto& t : threads) {t.join();}std::cout << "最终 shared_data 的值: " << shared_data << std::endl;return 0; }
2. std::unique_lock
:全能的指挥官
std::unique_lock
是一位能力超群的指挥官。它拥有 lock_guard
的所有优点,但其最大的特点是灵活。它不像 lock_guard
那样死板,你可以在任何时候手动加锁、解锁,甚至决定在创建时延迟加锁。这种灵活性使得它能应对更复杂的战术。
unique_lock
的真正力量体现在与 std::condition_variable
的协同作战中。在等待某个条件时,unique_lock
可以优雅地放下手中的锁,让出资源,直到被通知时再迅速重新锁定。这种合作机制是实现生产者-消费者模型、线程池等高级并发模式的核心。
-
核心特性:
- 灵活的加锁/解锁控制:提供
lock()
、unlock()
、try_lock()
等成员函数。 - 可延迟加锁:通过
std::defer_lock
构造,创建对象时不立即加锁。 - 可移动:可以作为函数参数或返回值,将锁的所有权转移。
- 与条件变量配合:是
std::condition_variable::wait()
函数唯一接受的锁类型。
- 灵活的加锁/解锁控制:提供
-
适用场景:
- 当你需要手动控制锁的加锁和解锁时。
- 当你需要与
std::condition_variable
配合,实现等待/通知机制时。 - 当你需要将锁的所有权从一个函数转移到另一个函数时。
-
示例:
#include <iostream> #include <thread> #include <mutex> #include <condition_variable>std::mutex mtx; std::condition_variable cv; bool ready = false;void worker_thread() {// 创建 unique_lock 对象,并锁定互斥量std::unique_lock<std::mutex> lock(mtx);std::cout << "工作线程正在等待条件..." << std::endl;// 等待条件变为 true,wait() 会原子地释放锁并进入休眠cv.wait(lock, []{ return ready; }); // 被唤醒后,wait() 会自动重新锁定互斥量std::cout << "工作线程被唤醒,开始处理数据。" << std::endl; }void main_thread() {std::this_thread::sleep_for(std::chrono::milliseconds(100));// 创建 unique_lock 并锁定互斥量std::unique_lock<std::mutex> lock(mtx);ready = true;std::cout << "主线程设置条件,并通知工作线程。" << std::endl;// 必须在通知前释放锁,以允许被唤醒的线程获取它lock.unlock(); cv.notify_one(); }int main() {std::thread t1(worker_thread);std::thread t2(main_thread);t1.join();t2.join();return 0; }
3. std::scoped_lock
:智慧的协调者
std::scoped_lock
是 C++17 引入的,它的主要作用是简化多互斥量加锁,并使用内置的死锁避免算法。你可以把它看作是 std::lock_guard
的多功能升级版。
-
核心特性:
- 同时锁定一个或多个互斥量。
- 内置死锁避免算法:它会以原子方式尝试锁定所有互斥量,如果失败则回滚并重试,确保不会因锁顺序不一致而死锁。
- 无手动控制:和
lock_guard
一样,不能手动加锁或解锁。
-
适用场景:
- 当你需要同时锁定多个互斥量时,这是最安全、最方便的选择。它彻底消除了死锁的风险。
-
示例:
#include <iostream> #include <thread> #include <mutex>std::mutex mtx1; std::mutex mtx2;void transfer_data_safe(int from_id, int to_id) {std::cout << "线程 " << std::this_thread::get_id() << " 正在尝试数据传输..." << std::endl;// 一次性锁定 mtx1 和 mtx2,使用内置的死锁避免算法// 无论哪个线程先获得哪个锁,都保证不会发生死锁std::scoped_lock lock(mtx1, mtx2);// 模拟数据传输std::this_thread::sleep_for(std::chrono::milliseconds(50));std::cout << "线程 " << std::this_thread::get_id() << " 安全地完成了数据传输。" << std::endl; } // lock 超出作用域,mtx1 和 mtx2 自动解锁int main() {std::thread t1(transfer_data_safe, 1, 2);std::thread t2(transfer_data_safe, 2, 1);t1.join();t2.join();return 0; }
4. std::shared_lock
:高效的图书馆管理员
std::shared_lock
是 C++14 引入的,它与 std::shared_mutex
(也叫读写锁)配合使用。它的主要作用是管理共享锁的所有权,提供一种允许多个线程同时读取,但只允许一个线程写入的同步机制。
-
核心特性:
- 共享锁模式:允许多个线程同时持有锁,用于并发读取。
- RAII 风格:在构造时加锁,在析构时自动解锁。
- 必须与
std::shared_mutex
配合使用。
-
适用场景:
- 读多写少的场景。当读取数据的频率远高于写入数据时,使用
shared_lock
可以显著提高程序的并发性能。
- 读多写少的场景。当读取数据的频率远高于写入数据时,使用
-
示例:
#include <iostream> #include <thread> #include <shared_mutex> #include <vector> #include <string>std::string shared_data = "initial"; std::shared_mutex shared_mtx;// 读者线程:只读取数据,可以并发执行 void reader_thread(int id) {// 构造 shared_lock 时,请求共享锁std::shared_lock<std::shared_mutex> lock(shared_mtx);std::cout << "读者 " << id << " 正在读取: " << shared_data << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(20)); } // lock 超出作用域,自动释放共享锁// 写者线程:修改数据,独占访问 void writer_thread(const std::string& new_data) {// 构造 unique_lock 时,请求独占锁,会阻塞所有读者和写者std::unique_lock<std::shared_mutex> lock(shared_mtx);std::cout << "写者正在写入..." << std::endl;shared_data = new_data;std::this_thread::sleep_for(std::chrono::milliseconds(50)); } // lock 超出作用域,自动释放独占锁int main() {std::vector<std::thread> readers;for (int i = 0; i < 5; ++i) {readers.emplace_back(reader_thread, i);}std::thread writer1(writer_thread, "new data");for (int i = 5; i < 10; ++i) {readers.emplace_back(reader_thread, i);}writer1.join();for (auto& t : readers) {t.join();}std::cout << "最终数据是: " << shared_data << std::endl;return 0; }
5. std::lock
:传统的锁定大师
std::lock
是一个函数,而不是一个类。它的主要作用是一次性锁定多个互斥量,并使用内置的死锁避免算法。
-
核心特性:
- 函数:不是 RAII 类,通常需要与
std::unique_lock
和std::defer_lock
配合使用。 - 死锁避免:通过其内部的算法,确保无论传入互斥量的顺序如何,都能安全地加锁。
- 函数:不是 RAII 类,通常需要与
-
适用场景:
- 在 C++11/14 版本中,是处理多锁死锁问题的标准方法。在 C++17 及以后,通常被
std::scoped_lock
所取代。
- 在 C++11/14 版本中,是处理多锁死锁问题的标准方法。在 C++17 及以后,通常被
-
示例:
#include <iostream> #include <thread> #include <mutex>std::mutex mtxA; std::mutex mtxB;void process_data_lock_function() {std::cout << "线程 " << std::this_thread::get_id() << " 正在处理数据..." << std::endl;// 延迟加锁,创建 unique_lock 对象但不立即锁定互斥量std::unique_lock<std::mutex> lockA(mtxA, std::defer_lock);std::unique_lock<std::mutex> lockB(mtxB, std::defer_lock);// 使用 std::lock 函数一次性锁定两个互斥量std::lock(lockA, lockB);std::cout << "线程 " << std::this_thread::get_id() << " 已安全地获取所有锁。" << std::endl;std::this_thread::sleep_for(std::chrono::milliseconds(100));// lockA 和 lockB 超出作用域时会自动解锁 }int main() {std::thread t1(process_data_lock_function);std::thread t2(process_data_lock_function);t1.join();t2.join();return 0; }
总结对比
特性 | std::lock_guard | std::unique_lock | std::scoped_lock | std::shared_lock |
---|---|---|---|---|
锁定类型 | 独占锁 | 独占锁 | 独占锁 | 共享锁 |
加锁数量 | 1 | 1 | 1或多个 | 1 |
灵活性 | 最低 | 最高 | 高 | 中等 |
死锁避免 | 无 | 无 | 内置 | 无 |
与条件变量 | 否 | 是 | 否 | 是 |
主要用途 | 简单、安全的单锁管理 | 需要灵活控制锁或与条件变量配合 | 安全的多锁管理 | 读多写少场景 |