跳到主要内容

C++ 互斥量

什么是互斥量?

在多线程编程中,当多个线程同时访问共享资源时,可能会导致数据竞争和不一致性问题。互斥量(Mutex,mutual exclusion的缩写)是一种同步原语,用于保护共享资源,确保在同一时刻只有一个线程可以访问该资源。

互斥量就像一把锁,任何想要访问共享资源的线程必须先获取(锁定)这把锁,使用完毕后再释放(解锁)它。如果某个线程已经锁定了互斥量,其他尝试锁定同一互斥量的线程将被阻塞,直到互斥量被解锁。

C++ 标准库中的互斥量类型

C++11开始提供了多线程支持,包括几种不同类型的互斥量,所有这些都在<mutex>头文件中定义:

  1. std::mutex:基本的互斥量
  2. std::recursive_mutex:允许同一线程多次获取锁的互斥量
  3. std::timed_mutex:提供超时功能的互斥量
  4. std::recursive_timed_mutex:结合了recursive_mutex和timed_mutex的功能

本文将主要关注最常用的std::mutex

基本用法:std::mutex

互斥量的基本操作

std::mutex提供以下基本操作:

  • lock():锁定互斥量,如果互斥量已被锁定,则阻塞当前线程
  • try_lock():尝试锁定互斥量,如果成功返回true,失败返回false(不阻塞)
  • unlock():解锁互斥量

让我们通过一个简单的例子来理解互斥量的基本用法:

cpp
#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_guardstd::unique_lock

std::lock_guard

std::lock_guard是最简单的锁管理器,提供基本的RAII风格锁管理:

cpp
#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_lockstd::lock_guard更灵活,提供了更多功能:

  • 支持延迟锁定(可以创建不立即锁定的对象)
  • 可以手动解锁和重新锁定
  • 支持条件变量的配合使用
  • 支持所有权转移
cpp
#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

死锁及其避免

死锁是多线程编程中的一个常见问题,当两个或多个线程互相等待对方释放资源时就会发生死锁。

死锁示例

cpp
#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

避免死锁的方法

  1. 保持一致的锁定顺序:始终以相同的顺序获取互斥量

  2. 使用std::lock同时锁定多个互斥量

cpp
#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;
}
  1. 使用std::scoped_lock(C++17)
cpp
#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;
}

实际应用案例:线程安全的计数器类

下面是一个线程安全的计数器类,它使用互斥量来保护计数操作:

cpp
#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

这个例子展示了如何创建一个线程安全的类,通过在类的方法中使用互斥量来保护其内部状态。无论有多少线程同时操作这个计数器,它的状态始终保持一致。

互斥量的性能考虑

虽然互斥量能够保护共享资源,但过度使用可能导致性能下降。每次锁定和解锁操作都有开销,而且线程等待锁会导致延迟。

一些优化技巧:

  1. 减少锁的粒度:只在必要的代码段使用锁
  2. 使用原子操作:对于简单的计数器等情况,考虑使用std::atomic
  3. 读写锁:如果是读多写少的情况,考虑使用std::shared_mutex(C++17)允许多个读取者同时访问

总结

互斥量是C++多线程编程中的基础同步工具,用于保护共享资源免受并发访问导致的数据竞争。本文介绍了:

  • 互斥量的基本概念及其在C++中的实现
  • 不同类型的互斥量:std::mutexstd::recursive_mutex
  • 锁管理器:std::lock_guardstd::unique_lock
  • 死锁问题及其避免方法
  • 线程安全类的实现方式

掌握互斥量的使用对于编写安全、高效的多线程程序至关重要。一旦理解了互斥量的基本概念,就可以进一步学习更高级的同步原语,如条件变量、原子操作和未来/承诺等。

练习

  1. 修改线程安全计数器类,添加一个reset()方法将计数器重置为0。
  2. 实现一个线程安全的简单日志类,支持多线程写入消息。
  3. 尝试使用std::recursive_mutex创建一个可以递归调用的线程安全函数。
  4. 实现一个使用std::unique_lock的线程安全资源池,允许线程获取和释放资源。
  5. 分析并修复一个死锁示例程序。

延伸阅读