C++学习:六个月从基础到就业——多线程编程:互斥量与锁
C++学习:六个月从基础到就业——多线程编程:互斥量与锁
本文是我C++学习之旅系列的第五十五篇技术文章,也是第四阶段"并发与高级主题"的第二篇,介绍C++11及以后标准中的互斥量(mutex)、锁(lock)等线程同步基础。查看完整系列目录了解更多内容。
引言
多线程程序中,多个线程对共享资源的访问会导致数据竞争和不确定行为。为保证数据一致性和线程安全,C++标准库提供了多种同步原语,其中最基础的就是互斥量(std::mutex
)和各种锁(std::lock_guard
、std::unique_lock
等)。本篇将系统讲解C++中的互斥量与锁的用法、原理及常见陷阱。
目录
- 多线程编程:互斥量与锁
- 引言
- 目录
- 互斥量基础
- std::mutex的基本用法
- std::lock_guard与RAII
- std::unique_lock与灵活锁管理
- 递归互斥量std::recursive_mutex
- 定时锁定std::timed_mutex
- 多锁与死锁预防
- std::lock和std::scoped_lock
- 避免死锁的常用策略
- 读写锁(共享互斥量)
- std::shared_mutex与std::shared_lock
- 实际应用案例
- 线程安全的计数器
- 线程安全的队列
- 常见问题与陷阱
- 死锁与活锁
- 锁粒度与性能
- 异常安全与锁
- 总结
互斥量基础
std::mutex的基本用法
std::mutex
是C++11标准库提供的最基础的互斥量类型。它用于保护临界区,确保同一时刻只有一个线程可以访问共享资源。
#include <iostream>
#include <thread>
#include <mutex>int counter = 0;
std::mutex counterMutex;void increment(int times) {for (int i = 0; i < times; ++i) {counterMutex.lock();++counter;counterMutex.unlock();}
}int main() {std::thread t1(increment, 100000);std::thread t2(increment, 100000);t1.join();t2.join();std::cout << "Final counter: " << counter << std::endl;return 0;
}
手动调用lock()
和unlock()
容易出错,推荐使用RAII风格的锁管理。
std::lock_guard与RAII
std::lock_guard
是最简单的RAII锁管理器,构造时加锁,析构时自动解锁,适合简单场景。
#include <mutex>std::mutex mtx;void safeFunction() {std::lock_guard<std::mutex> lock(mtx); // 构造时加锁// 临界区
} // 离开作用域自动解锁
优点:
- 避免忘记解锁
- 异常安全
std::unique_lock与灵活锁管理
std::unique_lock
比lock_guard
更灵活,支持延迟加锁、提前解锁、锁的转移等。
#include <mutex>std::mutex mtx;void flexibleFunction() {std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 不立即加锁// ...做一些准备...lock.lock(); // 手动加锁// ...临界区...lock.unlock(); // 手动解锁// ...非临界区...lock.lock(); // 重新加锁// ...再次进入临界区...
} // 离开作用域自动解锁(如果还持有锁)
unique_lock
还可以与std::lock
配合实现多互斥量的安全加锁。
递归互斥量std::recursive_mutex
递归互斥量允许同一线程多次获得同一个锁,适用于递归调用场景。
#include <mutex>std::recursive_mutex recMtx;void recursiveFunc(int n) {if (n <= 0) return;std::lock_guard<std::recursive_mutex> lock(recMtx);// ...临界区...recursiveFunc(n - 1); // 递归调用
}
注意:递归互斥量开销更大,非必要时应避免使用。
定时锁定std::timed_mutex
std::timed_mutex
和std::recursive_timed_mutex
支持超时锁定:
#include <mutex>
#include <chrono>
#include <iostream>std::timed_mutex tmtx;void tryLockWithTimeout() {if (tmtx.try_lock_for(std::chrono::milliseconds(100))) {std::cout << "Lock acquired" << std::endl;tmtx.unlock();} else {std::cout << "Timeout, lock not acquired" << std::endl;}
}
多锁与死锁预防
std::lock和std::scoped_lock
std::lock
可以一次性安全地锁定多个互斥量,避免死锁。C++17引入了std::scoped_lock
,更简洁:
#include <mutex>std::mutex m1, m2;void safeMultiLock() {std::scoped_lock lock(m1, m2); // 同时加锁,顺序无关// ...临界区...
}
避免死锁的常用策略
- 始终以相同顺序加锁
- 使用
std::lock
/std::scoped_lock
- 尽量缩小锁的作用域
- 避免在持锁时调用用户回调
读写锁(共享互斥量)
std::shared_mutex与std::shared_lock
C++17引入了std::shared_mutex
,允许多个线程同时读,但写时独占。
#include <shared_mutex>
#include <thread>
#include <iostream>std::shared_mutex rwMutex;
int sharedData = 0;void reader() {std::shared_lock lock(rwMutex);std::cout << "Read: " << sharedData << std::endl;
}void writer(int value) {std::unique_lock lock(rwMutex);sharedData = value;std::cout << "Write: " << sharedData << std::endl;
}
适用于读多写少的场景。
实际应用案例
线程安全的计数器
#include <mutex>class SafeCounter {int value = 0;std::mutex mtx;
public:void increment() {std::lock_guard<std::mutex> lock(mtx);++value;}int get() {std::lock_guard<std::mutex> lock(mtx);return value;}
};
线程安全的队列
#include <queue>
#include <mutex>
#include <condition_variable>
#include <optional>template<typename T>
class ThreadSafeQueue {std::queue<T> q;std::mutex mtx;std::condition_variable cv;
public:void push(T value) {{std::lock_guard<std::mutex> lock(mtx);q.push(std::move(value));}cv.notify_one();}std::optional<T> pop() {std::unique_lock<std::mutex> lock(mtx);if (q.empty()) return std::nullopt;T value = std::move(q.front());q.pop();return value;}// 支持阻塞等待T wait_and_pop() {std::unique_lock<std::mutex> lock(mtx);cv.wait(lock, [this]{ return !q.empty(); });T value = std::move(q.front());q.pop();return value;}
};
常见问题与陷阱
死锁与活锁
- 死锁:多个线程互相等待对方释放锁,导致程序卡死。
- 活锁:线程不断尝试获取锁但始终失败,导致无进展。
避免方法:使用std::lock
/std::scoped_lock
,保持加锁顺序一致,缩小锁粒度。
锁粒度与性能
- 粗粒度锁:简单但可能降低并发性能
- 细粒度锁:提高并发,但设计复杂,易出错
建议:根据实际需求权衡,优先保证正确性。
异常安全与锁
RAII锁(如lock_guard
、unique_lock
)能确保异常发生时自动释放锁,避免死锁。
总结
互斥量和锁是C++多线程同步的基础。合理使用std::mutex
、std::lock_guard
、std::unique_lock
、std::scoped_lock
等工具,可以有效避免数据竞争和死锁问题。C++17/20引入的shared_mutex
、scoped_lock
等进一步提升了并发性能和代码简洁性。
在下一篇文章中,我们将继续学习条件变量(std::condition_variable
),它是实现线程间通信和同步的关键工具。
这是我C++学习之旅系列的第五十五篇技术文章。查看完整系列目录了解更多内容。