c++多线程(4)------互斥同步
概念说明互斥量 (Mutex)用于保护共享资源,确保同一时刻只有一个线程访问RAII 锁 (lock_guard, unique_lock)自动管理锁生命周期,异常安全死锁预防使用 std::lock 同时锁定多个互斥量递归锁recursive_mutex,谨慎使用超时锁timed_mutex,避免无限等待最佳实践锁粒度小、临界区短、优先用 lock_guard。
- 操作系统:ubuntu22.04
- IDE:Visual Studio Code
- 编程语言:C++11
算法描述
C++11 引入了标准线程库()和同步原语(主要在 中),极大地简化了多线程编程。互斥同步是多线程编程中最基础、最重要的概念之一,用于保护共享数据,防止数据竞争(Data Race),确保线程安全。
一、为什么需要互斥同步?
当多个线程同时访问和修改同一个共享资源(如全局变量、静态变量、堆内存对象等)时,如果没有同步机制,会导致:
- 数据竞争 (Data Race):多个线程同时读写同一内存位置,结果不可预测。
- 脏读/脏写:一个线程读取到另一个线程写入过程中的“中间状态”。
- 程序崩溃或逻辑错误。
✅ 互斥同步的核心思想:同一时刻,只允许一个线程访问临界区(Critical Section)。
二、C++11 中的互斥量(Mutex)
C++11 在 头文件中提供了多种互斥量类型:
类型 | 描述 | 特点 |
---|---|---|
std::mutex | 最基本的互斥量 | 不可递归,不可复制,不可移动 |
std::recursive_mutex | 递归互斥量 | 同一线程可多次加锁,需对应次数解锁 |
std::timed_mutex | 带超时的互斥量 | 支持 try_lock_for, try_lock_until |
std::recursive_timed_mutex | 递归+超时互斥量 | 结合上述两者特性 |
std::mutex 基本用法
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx; // 互斥量
int shared_data = 0;
void increment()
{
for (int i = 0; i < 100000; ++i)
{
mtx.lock(); // 加锁
++shared_data; // 临界区
mtx.unlock(); // 解锁
}
}
int main()
{
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
std::cout << "Result: " << shared_data << std::endl; // 正确输出 200000
return 0;
}
问题:如果在 mtx.lock() 和 mtx.unlock() 之间发生异常,可能导致永远不释放锁(死锁)!
三、RAII 锁管理:std::lock_guard 和 std::unique_lock
为避免手动管理锁导致的问题,C++11 引入了 RAII(Resource Acquisition Is Initialization)风格的锁管理类。
✅ 1. std::lock_guard(推荐用于简单场景)
- 构造时自动加锁。
- 析构时自动解锁。
- 不可手动 unlock,不可转移所有权。
- 适用于作用域明确、无条件加锁的场景。
void increment_safe()
{
for (int i = 0; i < 100000; ++i)
{
std::lock_guard<std::mutex> lock(mtx); // 自动加锁
++shared_data; // 临界区
// 自动解锁(离开作用域时析构)
}
}
异常安全:即使临界区内抛出异常,lock_guard 也会在栈展开时析构并解锁。
2. std::unique_lock(更灵活)
- 支持延迟加锁、手动解锁、条件变量配合。
- 可转移所有权(moveable)。
- 性能略低于 lock_guard(因为要维护锁状态)。
void flexible_lock()
{
std::unique_lock<std::mutex> lock(mtx, std::defer_lock); // 延迟加锁
// ... 做一些不需锁的操作 ...
lock.lock(); // 手动加锁
shared_data++;
lock.unlock(); // 手动解锁
// ... 做其他事 ...
if (!lock.owns_lock())
{
lock.lock();
shared_data++;
// 自动解锁(析构时)
}
}
常用于配合 std::condition_variable:
std::mutex mtx;
std::condition_variable cv;
bool ready = false;
void worker()
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, []{ return ready; }); // 自动解锁等待,唤醒后自动加锁
// do work...
}
四、避免死锁:std::lock 和 std::adopt_lock
当需要同时锁定多个互斥量时,如果顺序不一致,容易导致死锁。
✅ 使用 std::lock 同时锁定多个互斥量(避免死锁)
std::mutex m1, m2;
void transfer(Account &from, Account &to, int amount)
{
std::lock(m1, m2); // 同时锁定,内部避免死锁
std::lock_guard<std::mutex> lock1(m1, std::adopt_lock);
std::lock_guard<std::mutex> lock2(m2, std::adopt_lock);
from.balance -= amount;
to.balance += amount;
}
std::adopt_lock:告诉 lock_guard,锁已经被获取,只需在析构时释放
五、带超时的互斥量:std::timed_mutex
适用于不想无限等待锁的场景。
std::timed_mutex tmtx;
void try_lock_with_timeout()
{
if (tmtx.try_lock_for(std::chrono::milliseconds(100)))
{
// 成功获取锁
// ... 临界区 ...
tmtx.unlock();
}
else
{
std::cout << "Failed to acquire lock within 100ms.\n";
}
}
六、递归互斥量:std::recursive_mutex
允许同一线程多次加锁(必须对应次数解锁)。
std::recursive_mutex rmtx;
void recursive_func(int depth)
{
std::lock_guard<std::recursive_mutex> lock(rmtx);
if (depth > 0)
{
recursive_func(depth - 1); // 递归调用,不会死锁
}
}
虽然方便,但应尽量避免使用,因为它掩盖了设计问题。更好的做法是重构代码,避免需要递归加锁。
七、性能与最佳实践
互斥量类型 | 性能 | 适用场景 |
---|---|---|
std::mutex + lock_guard | 最高 | 绝大多数简单同步场景 |
std::unique_lock | 中等 | 需要手动控制锁、配合条件变量 |
std::timed_mutex | 较低 | 需要超时机制 |
std::recursive_mutex | 较低 | 仅在无法避免递归加锁时使用 |
最佳实践:
- 优先使用 std::lock_guard — 简单、安全、高效。
- 临界区尽量小 — 减少锁持有时间,提高并发性。
- 避免在持有锁时调用用户回调或可能阻塞的函数 — 防止死锁或性能下降。
- 多个互斥量时,使用 std::lock 避免死锁。
- 不要复制或移动互斥量对象 — 它们是不可复制、不可移动的。
- 析构前确保没有线程持有锁 — 否则行为未定义。
八、常见错误
❌ 1. 忘记解锁(使用裸 mutex)
mtx.lock();
// ... 可能抛异常 ...
mtx.unlock(); // 可能永远不执行!
✅ 改用 lock_guard 或 unique_lock。
❌ 2. 在析构时仍有线程持有锁
class BadExample
{
std::mutex mtx;
public:
void do_something()
{
mtx.lock();
// 长时间操作…
} // 忘记 unlock!
~BadExample() { /* mtx 可能仍被锁住!未定义行为 */ }
};
✅ 确保锁的生命周期短于互斥量。
❌ 3. 死锁(交叉加锁)
// Thread 1:
lock(m1);
lock(m2);
// Thread 2:
lock(m2);
lock(m1); // 死锁!
✅ 使用 std::lock(m1, m2); 同时加锁。
九、完整示例:线程安全计数器
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
class ThreadSafeCounter
{
mutable std::mutex mtx; // mutable 允许 const 函数加锁
int count = 0;
public:
void increment()
{
std::lock_guard<std::mutex> lock(mtx);
++count;
}
int get() const
{
std::lock_guard<std::mutex> lock(mtx);
return count;
}
};
int main()
{
ThreadSafeCounter counter;
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i)
{
threads.emplace_back([&counter]()
{
for (int j = 0; j < 10000; ++j)
{
counter.increment();
}
});
}
for (auto& t : threads) {
t.join();
}
std::cout << "Final count: " << counter.get() << std::endl; // 100000
return 0;
}
总结
概念 | 说明 |
---|---|
互斥量 (Mutex) | 用于保护共享资源,确保同一时刻只有一个线程访问 |
RAII 锁 (lock_guard, unique_lock) | 自动管理锁生命周期,异常安全 |
死锁预防 | 使用 std::lock 同时锁定多个互斥量 |
递归锁 | recursive_mutex,谨慎使用 |
超时锁 | timed_mutex,避免无限等待 |
最佳实践 | 锁粒度小、临界区短、优先用 lock_guard |
更多推荐
所有评论(0)