C++ 互斥量
什么是互斥量?
在多线程编程中,当多个线程同时访问共享资源时,可能会导致数据竞争和不一致性问题。互斥量(Mutex,mutual exclusion的缩写)是一种同步原语,用于保护共享资源,确保在同一时刻只有一个线程可以访问该资源。
互斥量就像一把锁,任何想要访问共享资源的线程必须先获取(锁定)这把锁,使用完毕后再释放(解锁)它。如果某个线程已经锁定了互斥量,其他尝试锁定同一互斥量的线程将被阻塞,直到互斥量被解锁。
C++ 标准库中的互斥量类型
C++11开始提供了多线程支持,包括几种不同类型的互斥量,所有这些都在<mutex>
头文件中定义:
std::mutex
:基本的互斥量std::recursive_mutex
:允许同一线程多次获取锁的互斥量std::timed_mutex
:提供超时功能的互斥量std::recursive_timed_mutex
:结合了recursive_mutex和timed_mutex的功能
本文将主要关注最常用的std::mutex
。
基本用法:std::mutex
互斥量的基本操作
std::mutex
提供以下基本操作:
lock()
:锁定互斥量,如果互斥量已被锁定,则阻塞当前线程try_lock()
:尝试锁定互斥量,如果成功返回true
,失败返回false
(不阻塞)unlock()
:解锁互斥量
让我们通过一个简单的例子来理解互斥量的基本用法:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 创建互斥量
int shared_counter = 0; // 共享资源
void increment_counter(int id, int iterations) {
for (int i = 0; i < iterations; ++i) {
mtx.lock(); // 锁定互斥量
++shared_counter; // 访问共享资源
std::cout << "Thread " << id << " incremented counter to: " << shared_counter << std::endl;
mtx.unlock(); // 解锁互斥量
// 模拟其他工作
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int main() {
std::thread t1(increment_counter, 1, 5);
std::thread t2(increment_counter, 2, 5);
t1.join();
t2.join();
std::cout << "Final counter value: " << shared_counter << std::endl;
return 0;
}
输出示例:
Thread 1 incremented counter to: 1
Thread 2 incremented counter to: 2
Thread 1 incremented counter to: 3
Thread 2 incremented counter to: 4
Thread 1 incremented counter to: 5
Thread 2 incremented counter to: 6
Thread 1 incremented counter to: 7
Thread 2 incremented counter to: 8
Thread 1 incremented counter to: 9
Thread 2 incremented counter to: 10
Final counter value: 10
在这个例子中,两个线程交替增加共享计数器的值。通过使用互斥量,我们确保了在任何时候只有一个线程可以修改shared_counter
,从而避免了数据竞争。
直接使用lock()
和unlock()
容易出错!如果在unlock()
之前代码出现异常,互斥量可能永远不会被释放,导致死锁。因此,推荐使用RAII风格的锁管理器。
锁管理器:RAII方式管理互斥量
C++标准库提供了几种锁管理器,它们采用RAII(资源获取即初始化)设计模式,当对象构造时获取锁,当对象析构时释放锁。最常用的锁管理器是std::lock_guard
和std::unique_lock
。
std::lock_guard
std::lock_guard
是最简单的锁管理器,提供基本的RAII风格锁管理:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int shared_counter = 0;
void increment_counter(int id, int iterations) {
for (int i = 0; i < iterations; ++i) {
{
std::lock_guard<std::mutex> lock(mtx); // 构造时锁定互斥量
++shared_counter;
std::cout << "Thread " << id << " incremented counter to: " << shared_counter << std::endl;
// 离开作用域时自动解锁
}
std::this_thread::sleep_for(std::chrono::milliseconds(10));
}
}
int main() {
std::thread t1(increment_counter, 1, 5);
std::thread t2(increment_counter, 2, 5);
t1.join();
t2.join();
std::cout << "Final counter value: " << shared_counter << std::endl;
return 0;
}
std::unique_lock
std::unique_lock
比std::lock_guard
更灵活,提供了更多功能:
- 支持延迟锁定(可以创建不立即锁定的对象)
- 可以手动解锁和重新锁定
- 支持条件变量的配合使用
- 支持所有权转移
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx;
int shared_data = 0;
void process_data(int id) {
std::unique_lock<std::mutex> lock(mtx); // 构造时锁定
// 修改共享数据
shared_data += id;
std::cout << "Thread " << id << " processing data: " << shared_data << std::endl;
// 暂时释放锁,执行不需要互斥的操作
lock.unlock();
std::cout << "Thread " << id << " doing some work without lock" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// 重新获取锁
lock.lock();
shared_data *= 2;
std::cout << "Thread " << id << " finished processing: " << shared_data << std::endl;
// 离开作用域时自动解锁
}
int main() {
std::thread t1(process_data, 1);
std::thread t2(process_data, 2);
t1.join();
t2.join();
return 0;
}
输出示例:
Thread 1 processing data: 1
Thread 1 doing some work without lock
Thread 2 processing data: 3
Thread 2 doing some work without lock
Thread 1 finished processing: 6
Thread 2 finished processing: 12
死锁及其避免
死锁是多线程编程中的一个常见问题,当两个或多个线程互相等待对方释放资源时就会发生死锁。
死锁示例
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1, mutex2;
void thread_1() {
// 先锁定mutex1,再锁定mutex2
mutex1.lock();
std::cout << "Thread 1: Locked mutex1" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 增加死锁发生的机会
mutex2.lock();
std::cout << "Thread 1: Locked mutex2" << std::endl;
// 操作共享资源
mutex2.unlock();
mutex1.unlock();
}
void thread_2() {
// 先锁定mutex2,再锁定mutex1
mutex2.lock();
std::cout << "Thread 2: Locked mutex2" << std::endl;
std::this_thread::sleep_for(std::chrono::milliseconds(100));
mutex1.lock();
std::cout << "Thread 2: Locked mutex1" << std::endl;
// 操作共享资源
mutex1.unlock();
mutex2.unlock();
}
int main() {
std::thread t1(thread_1);
std::thread t2(thread_2);
t1.join();
t2.join();
return 0;
}
这个程序可能会陷入死锁,因为thread_1
持有mutex1
并等待mutex2
,而同时thread_2
持有mutex2
并等待mutex1
。
避免死锁的方法
-
保持一致的锁定顺序:始终以相同的顺序获取互斥量
-
使用
std::lock
同时锁定多个互斥量:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1, mutex2;
void thread_safe(int id) {
std::lock(mutex1, mutex2); // 原子方式锁定两个互斥量,避免死锁
// 使用已锁定的互斥量创建lock_guard(采用adopt_lock参数)
std::lock_guard<std::mutex> lock1(mutex1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(mutex2, std::adopt_lock);
std::cout << "Thread " << id << " locked both mutexes" << std::endl;
// 操作共享资源
// 离开作用域时自动解锁
}
int main() {
std::thread t1(thread_safe, 1);
std::thread t2(thread_safe, 2);
t1.join();
t2.join();
return 0;
}
- 使用
std::scoped_lock
(C++17):
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1, mutex2;
void thread_safe_cpp17(int id) {
// C++17的scoped_lock可以同时锁定多个互斥量
std::scoped_lock lock(mutex1, mutex2);
std::cout << "Thread " << id << " locked both mutexes using scoped_lock" << std::endl;
// 操作共享资源
// 离开作用域时自动解锁
}
int main() {
std::thread t1(thread_safe_cpp17, 1);
std::thread t2(thread_safe_cpp17, 2);
t1.join();
t2.join();
return 0;
}
实际应用案例:线程安全的计数器类
下面是一个线程安全的计数器类,它使用互斥量来保护计数操作:
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
class ThreadSafeCounter {
private:
mutable std::mutex mtx; // mutable允许在const方法中修改
int value;
public:
ThreadSafeCounter() : value(0) {}
// 增加计数
void increment() {
std::lock_guard<std::mutex> lock(mtx);
++value;
}
// 减少计数
void decrement() {
std::lock_guard<std::mutex> lock(mtx);
--value;
}
// 获取当前值
int get() const {
std::lock_guard<std::mutex> lock(mtx);
return value;
}
};
void worker(ThreadSafeCounter& counter, int iterations) {
for (int i = 0; i < iterations; ++i) {
counter.increment();
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
int main() {
ThreadSafeCounter counter;
// 创建10个线程,每个线程增加计数器1000次
std::vector<std::thread> threads;
int num_threads = 10;
int iterations_per_thread = 1000;
for (int i = 0; i < num_threads; ++i) {
threads.push_back(std::thread(worker, std::ref(counter), iterations_per_thread));
}
// 等待所有线程完成
for (auto& t : threads) {
t.join();
}
std::cout << "Expected value: " << num_threads * iterations_per_thread << std::endl;
std::cout << "Actual value: " << counter.get() << std::endl;
return 0;
}
输出示例:
Expected value: 10000
Actual value: 10000
这个例子展示了如何创建一个线程安全的类,通过在类的方法中使用互斥量来保护其内部状态。无论有多少线程同时操作这个计数器,它的状态始终保持一致。
互斥量的性能考虑
虽然互斥量能够保护共享资源,但过度使用可能导致性能下降。每次锁定和解锁操作都有开销,而且线程等待锁会导致延迟。
一些优化技巧:
- 减少锁的粒度:只在必要的代码段使用锁
- 使用原子操作:对于简单的计数器等情况,考虑使用
std::atomic
- 读写锁:如果是读多写少的情况,考虑使用
std::shared_mutex
(C++17)允许多个读取者同时访问
总结
互斥量是C++多线程编程中的基础同步工具,用于保护共享资源免受并发访问导致的数据竞争。本文介绍了:
- 互斥量的基本概念及其在C++中的实现
- 不同类型的互斥量:
std::mutex
、std::recursive_mutex
等 - 锁管理器:
std::lock_guard
和std::unique_lock
- 死锁问题及其避免方法
- 线程安全类的实现方式
掌握互斥量的使用对于编写安全、高效的多线程程序至关重要。一旦理解了互斥量的基本概念,就可以进一步学习更高级的同步原语,如条件变量、原子操作和未来/承诺等。
练习
- 修改线程安全计数器类,添加一个
reset()
方法将计数器重置为0。 - 实现一个线程安全的简单日志类,支持多线程写入消息。
- 尝试使用
std::recursive_mutex
创建一个可以递归调用的线程安全函数。 - 实现一个使用
std::unique_lock
的线程安全资源池,允许线程获取和释放资源。 - 分析并修复一个死锁示例程序。
延伸阅读
- C++ Reference: std::mutex
- C++ Reference: std::lock_guard
- C++ Reference: std::unique_lock
- 《C++ Concurrency in Action》by Anthony Williams