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资源。
于是,我们使用条件变量来完成,通过消费者生产一个产品后,就通知消费者消费;只要生产者不通知,消费者就不会继续进行(在第一次检测队列发现为空后);
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步