C++多线程——mutex、unique_lock、condition_variable
互斥锁 std::mutex
当涉及到多线程编程时,互斥锁(Mutex)是一种同步机制,用于保护共享资源免受并发访问的影响。以下是一个简单的C++互斥锁示例:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx; // 创建一个互斥锁
void printNumbers(int n) {
for (int i = 0; i < n; ++i) {
mtx.lock(); // 上锁
std::cout << i << " ";
mtx.unlock(); // 解锁
}
}
int main() {
const int numThreads = 3;
const int numIterations = 5;
std::thread threads[numThreads];
for (int i = 0; i < numThreads; ++i) {
threads[i] = std::thread(printNumbers, numIterations);
}
for (int i = 0; i < numThreads; ++i) {
threads[i].join();
}
return 0;
}
在上面的示例中,我们创建了一个互斥锁 mtx
。printNumbers
函数打印一些数字,并在每次打印之前上锁,然后在打印完成后解锁。这确保了每个线程都能按顺序打印数字,而不会产生竞态条件。请注意,使用互斥锁时要小心避免死锁(Deadlock)。死锁发生在两个或多个线程相互等待对方释放锁的情况下。
死锁 deadlock
这里举例给出死锁的例子
考虑以下场景:有两个线程A和B,它们都试图获取两个互斥锁(Mutex),但它们的获取顺序不一致。这可能导致死锁,因为A锁住了第一个互斥锁并等待第二个,而B锁住了第二个互斥锁并等待第一个。
以下是一个示例代码,演示了这种死锁情况:
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1;
std::mutex mutex2;
void ThreadA() {
std::unique_lock<std::mutex> lock1(mutex1);
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些工作
std::cout << "Thread A 已经锁住 mutex1,尝试获取 mutex2...\n";
std::unique_lock<std::mutex> lock2(mutex2); // 试图获取 mutex2
std::cout << "Thread A 成功获取 mutex2!\n";
// 在这里执行一些工作...
}
void ThreadB() {
std::unique_lock<std::mutex> lock2(mutex2);
std::this_thread::sleep_for(std::chrono::seconds(1)); // 模拟一些工作
std::cout << "Thread B 已经锁住 mutex2,尝试获取 mutex1...\n";
std::unique_lock<std::mutex> lock1(mutex1); // 试图获取 mutex1
std::cout << "Thread B 成功获取 mutex1!\n";
// 在这里执行一些工作...
}
int main() {
std::thread t1(ThreadA);
std::thread t2(ThreadB);
t1.join();
t2.join();
return 0;
}
在这个示例中,ThreadA尝试获取mutex1,而ThreadB尝试获取mutex2,它们的获取顺序不一致,因此可能导致死锁。当你运行这个程序时,你会看到两个线程都被锁住,并且程序不会继续执行下去。
std::lock_guard
与 std::unique_lock
lock_guard
std::lock_guard
是 C++ 标准库中的一种互斥量封装类,用于保护共享数据,防止多个线程同时访问同一资源而导致的数据竞争问题。
std::lock_guard
的特点如下:
-
当构造函数被调用时,该互斥量会被自动锁定。
-
当析构函数被调用时,该互斥量会被自动解锁。
-
std::lock_guard 对象不能复制或移动,因此它只能在局部作用域中使用。
std::unique_lock
std::unique_lock
是 C++ 标准库中提供的一个互斥量封装类,用于在多线程程序中对互斥量进行加锁和解锁操作。它的主要特点是可以对互斥量进行更加灵活的管理,包括延迟加锁、条件变量、超时等。
std::unique_lock 提供了以下几个成员函数:
-
lock()
:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁。 -
try_lock()
:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则函数立即返回 false,否则返回 true。 -
try_lock_for(const std::chrono::duration<Rep, Period>& rel_time)
:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间。 -
try_lock_until(const std::chrono::time_point<Clock, Duration>& abs_time)
:尝试对互斥量进行加锁操作,如果当前互斥量已经被其他线程持有,则当前线程会被阻塞,直到互斥量被成功加锁,或者超过了指定的时间点。 -
unlock()
:对互斥量进行解锁操作。
除了上述成员函数外,std::unique_lock 还提供了以下几个构造函数:
-
unique_lock() noexcept = default
:默认构造函数,创建一个未关联任何互斥量的 std::unique_lock 对象。 -
explicit unique_lock(mutex_type& m)
:构造函数,使用给定的互斥量 m 进行初始化,并对该互斥量进行加锁操作。 -
unique_lock(mutex_type& m, defer_lock_t) noexcept
:构造函数,使用给定的互斥量 m 进行初始化,但不对该互斥量进行加锁操作。 -
unique_lock(mutex_type& m, try_to_lock_t) noexcept
:构造函数,使用给定的互斥量 m 进行初始化,并尝试对该互斥量进行加锁操作。如果加锁失败,则创建的 std::unique_lock 对象不与任何互斥量关联。 -
unique_lock(mutex_type& m, adopt_lock_t) noexcept
:构造函数,使用给定的互斥量 m 进行初始化,并假设该互斥量已经被当前线程成功加锁。
条件变量 condition_variable
条件变量具有“通知”的作用,利用变量的值为true或false,可以达到通知的作用。以下是使用条件变量来实现简单的生产者消费者模型。
#include <iostream>
#include <queue>
#include <mutex>
#include <condition_variable>
#include <thread>
std::queue<int> queue;
std::mutex mtx;
std::condition_variable cv;
void Producer() {
for (int i = 0; i < 10; ++i) {
{
std::unique_lock<std::mutex> lock(mtx);
queue.push(i);
cv.notify_one();
std::cout << "Producer : " << i << std::endl;
}
std::this_thread::sleep_for(std::chrono::microseconds (1000));
}
}
void Customer() {
while (1) {
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [] () {
return !queue.empty();
});
int res = queue.front();
queue.pop();
std::cout << "Customer : " << res << std::endl;
}
}
int main() {
std::thread t1(Producer);
std::thread t2(Customer);
t1.join();
t2.join();
return 0;
}
完成这个程序题需要的合理的思考步骤:
-
首先实现生产者和消费者基本逻辑
-
由于两者共享一个队列,可能存在数据竞态的问题,于是需要加锁 mutex,用 unique_lock 来实现。
-
队列中为空时,消费者不能取出数据,这一条件可实现为:
消费者在准备消费前判断队列是否为空,如果是空,就等着;如果不为空,就消费。
如果直接使用while循环进行empty判空,仍然可行,但无疑会耗费CPU资源。
于是,我们使用条件变量来完成,通过消费者生产一个产品后,就通知消费者消费;只要生产者不通知,消费者就不会继续进行(在第一次检测队列发现为空后);