文章目录
前言
在C++的多线程编程中,互斥锁的管理是确保数据一致性和线程同步的关键。std::unique_lock和std::lock_guard是两种用于管理互斥锁的智能锁对象,它们提供了便捷且安全的方式来处理并发访问共享资源的问题。下面我们来详细探讨这两种机制。
一、使用std::lock_guard与std::unique_lock管理锁
1、std::lock_guard
lock_guard
的实现原理主要基于 RAII(Resource Acquisition Is Initialization)原则,这是 C++ 中的一个重要编程技巧。RAII
意味着资源的获取(如内存分配、文件打开、互斥锁锁定等)与对象的初始化绑定在一起,而资源的释放(如内存释放、文件关闭、互斥锁解锁等)则与对象的析构绑定在一起。lock_guard
的实现通常包括以下几个部分:
- 构造函数:
lock_guard
的构造函数接受一个互斥锁(或其他类型的锁)作为参数,并在构造函数内部调用该互斥锁的lock()
方法来锁定互斥锁。这样,当lock_guard
对象被创建时,互斥锁就被自动锁定了。 - 析构函数:
lock_guard
的析构函数负责在对象生命周期结束时自动调用互斥锁的unlock()
方法来解锁互斥锁。由于析构函数在对象离开其作用域时自动被调用,因此这确保了互斥锁在不再需要时会被正确地解锁,即使在异常情况下也是如此。 - 禁止复制和赋值:
lock_guard
的实现通常会禁止对象的复制和赋值操作,这是为了避免出现多个lock_guard
对象持有同一个互斥锁的情况,从而防止死锁的发生。
下面是一个简化的
lock_guard
实现示例:
template <typename Mutex>
class lock_guard {
public:
explicit lock_guard(Mutex& mtx) : mutex_(mtx) {
mutex_.lock(); // 在构造函数中锁定互斥锁
}
~lock_guard() {
mutex_.unlock(); // 在析构函数中解锁互斥锁
}
// 禁止复制和赋值
lock_guard(const lock_guard&) = delete;
lock_guard& operator=(const lock_guard&) = delete;
private:
Mutex& mutex_; // 持有对互斥锁的引用
};
在这个示例中,
lock_guard
是一个模板类,它可以接受不同类型的互斥锁作为参数。在构造函数中,它通过调用mutex_.lock()
来锁定互斥锁。当lock_guard
对象离开其作用域时,析构函数会被自动调用,从而解锁互斥锁。此外,复制构造函数和赋值运算符被显式删除,以防止lock_guard
对象的复制和赋值。
使用
lock_guard
可以极大地简化多线程编程中的互斥锁管理,同时确保在异常安全的情况下正确地解锁互斥锁。
2、std::unique_lock
unique_lock
是 C++11 标准库中提供的一个类模板,用于封装和管理互斥量(mutex),以实现多线程同步。unique_lock
的实现原理相较于lock_guard
更为复杂和灵活,因为它提供了更多的控制选项和功能。它不仅允许在运行时锁定和解锁互斥锁,还可以配合条件变量使用,以实现线程间的精确同步。unique_lock
的实现通常包含以下几个关键方面:
-
互斥量的所有权:
unique_lock
对象在构造时可以接收一个互斥量的引用,并可以选择立即锁定该互斥量。一旦unique_lock
对象拥有了互斥量的所有权,它就可以控制互斥量的锁定和解锁状态。 -
灵活的锁定策略:
unique_lock
提供了多种锁定策略,可以通过构造函数或成员函数来指定。例如,可以使用std::defer_lock
策略来延迟锁定互斥量,或者使用std::try_to_lock
策略来尝试锁定互斥量(如果互斥量已经被其他线程锁定,则立即返回)。 -
条件变量的支持:
unique_lock
可以与条件变量(std::condition_variable
)一起使用,以实现线程间的同步和通信。通过unique_lock
对象,线程可以等待条件变量触发,或者通知其他线程条件已经满足。 -
所有权的转移:
unique_lock
允许在构造时或在任意时刻通过移动操作(move operation)将互斥量的所有权转移给其他unique_lock
对象。这种特性使得unique_lock
在处理复杂的多线程逻辑时更加灵活。 -
析构函数的行为:
unique_lock
的析构函数会自动解锁互斥量(如果当前对象拥有互斥量的所有权)。这种自动解锁的行为保证了即使在异常情况下,互斥量也能被正确释放。
下面是一个简化的
unique_lock
实现示例:
template <typename Mutex>
class unique_lock {
public:
// 构造函数,可以选择立即锁定互斥量
explicit unique_lock(Mutex& mtx, std::defer_lock_t) : mutex_(mtx), owns_lock_(false) {}
unique_lock(Mutex& mtx, std::try_to_lock_t) : mutex_(mtx), owns_lock_(try_lock(mtx)) {}
explicit unique_lock(Mutex& mtx) : mutex_(mtx), owns_lock_(true) { lock(mtx); }
// 解锁互斥量(如果当前对象拥有互斥量的所有权)
void unlock() {
if (owns_lock_) {
mutex_.unlock();
owns_lock_ = false;
}
}
// 析构函数,自动解锁互斥量(如果当前对象拥有互斥量的所有权)
~unique_lock() {
unlock();
}
// 移动构造函数,转移互斥量的所有权
unique_lock(unique_lock&& other) noexcept : mutex_(other.mutex_), owns_lock_(other.owns_lock_) {
other.owns_lock_ = false;
}
// 其他成员函数和成员变量...
private:
Mutex& mutex_; // 持有对互斥量的引用
bool owns_lock_; // 指示当前对象是否拥有互斥量的所有权
// 实现锁定和尝试锁定的辅助函数...
};
在这个示例中,
unique_lock
是一个模板类,它可以接受不同类型的互斥量作为参数。它使用了一个布尔变量owns_lock_
来跟踪是否拥有互斥量的所有权。构造函数根据不同的参数类型(如std::defer_lock_t
、std::try_to_lock_t
或无参数)来决定是否立即锁定互斥量。析构函数会自动解锁互斥量(如果当前对象拥有所有权)。此外,unique_lock
还提供了移动构造函数,以支持所有权的转移。
unique_lock
的灵活性和功能使其在多线程编程中非常有用,特别是在需要更复杂的同步逻辑和条件变量使用时。然而,由于它的复杂性,相比lock_guard
,unique_lock
的性能可能会稍低一些。因此,在选择使用lock_guard
还是unique_lock
时,应根据具体的需求和场景来权衡。
3、std::unique_lock与std::condition_variable配合使用
std::unique_lock
与std::condition_variable
配合使用是多线程编程中实现线程间同步和通信的强大机制。条件变量允许一个或多个线程等待直到满足特定条件,而unique_lock
提供了必要的锁机制来安全地检查这些条件并管理线程的睡眠和唤醒。以下是std::unique_lock
与std::condition_variable
配合使用的基本步骤:
- 创建一个互斥锁
std::mutex
和一个条件变量std::condition_variable
。 - 在需要等待条件的线程中使用
std::unique_lock
锁定互斥锁。 - 线程调用条件变量的
wait()
方法,传入unique_lock
对象,以及一个可选的lambda表达式(谓词)来检查等待的条件是否已满足。如果条件不满足,wait()
方法将释放锁并使线程进入睡眠状态。 - 当其他线程修改了共享资源并满足了条件后,它们会通知条件变量,这通常通过调用
notify_one()
或notify_all()
方法来完成。 - 当条件变量收到通知后,等待的线程将被唤醒,重新锁定互斥锁,并再次检查条件是否确实已经满足(这个过程称为“条件变量检查”)。
- 如果条件满足,线程继续执行;否则,它将再次调用
wait()
方法进入等待状态。
以下是一个示例代码,展示了如何结合使用
std::unique_lock
和std::condition_variable
:
#include <iostream>
#include <thread>
#include <mutex>
#include <condition_variable>
std::mutex mtx;
std::condition_variable cv;
bool ready = false; // 共享资源,表示某个条件是否已满足
void workerThread() {
std::unique_lock<std::mutex> lck(mtx);
while (!ready) { // 等待直到条件满足
cv.wait(lck, []{ return ready; }); // 在这里线程可能被阻塞
}
// 继续处理其他任务...
}
int main() {
std::thread t(workerThread);
// ... 在其他线程中进行一些操作,最终设置 ready = true
{
std::lock_guard<std::mutex> lock(mtx);
ready = true; // 修改共享资源
}
cv.notify_one(); // 通知等待的线程
t.join();
return 0;
}
在上面的示例中,工作线程会等待
ready
变量变为true
。一旦主线程设置了ready
并调用notify_one()
,工作线程就会从wait()
调用中返回,然后检查条件,并在确认条件已满足后继续执行。这种配合使用的方式确保了线程在条件不满足时不会占用不必要的CPU周期,同时在条件满足时能及时醒来进行处理。