高阶开发基础——快速入门C++并发编程3
目录
关于互斥量死锁
解决方案
顺序上锁。
超时机制
使用RAII来保证锁的恰当释放
关于互斥量死锁
上一篇博客中,我们看到了mutex可以保护数据,但是这也引来了新的麻烦,那就是互斥量的死锁。
互斥量(Mutex)是用于保护共享资源的一种同步机制。死锁(Deadlock)是指多个线程或进程因竞争资源而陷入相互等待的状态,导致程序无法继续执行。死锁通常发生在以下四个条件同时满足时:
-
互斥条件:资源一次只能被一个线程占用。
-
占有并等待:线程持有至少一个资源,并等待获取其他被占用的资源。
-
非抢占条件:线程已持有的资源不能被其他线程强行抢占,只能由线程主动释放。
-
循环等待条件:存在一个线程的循环链,每个线程都在等待下一个线程所持有的资源。
说的很空,我们现在来看一段代码:
#include <iostream> #include <thread> #include <mutex> std::mutex mutex1; std::mutex mutex2; void thread1() { std::lock_guard<std::mutex> lock1(mutex1); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟一些操作 std::lock_guard<std::mutex> lock2(mutex2); std::cout << "Thread 1 finished" << std::endl; } void thread2() { std::lock_guard<std::mutex> lock2(mutex2); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟一些操作 std::lock_guard<std::mutex> lock1(mutex1); std::cout << "Thread 2 finished" << std::endl; } int main() { std::thread t1(thread1); std::thread t2(thread2); t1.join(); t2.join(); return 0; }
上面的代码就是一段死锁代码。分析一下,thread1先启动了,快速的占用了mutex1,thread2随后占用了mutex1.麻烦来了,现在做好了一系列操作后,我们马上有需要获取第二个锁,对于thread1而言,他要拿到mutex2,就必须让thread2放掉他自己手中的mutex2,但是,thread2想要放掉自己的mutex2,就必须拿到thread1手中的mutex1...好!你一眼发现,这是双方都盯着对方的资源而不肯松手自己的资源导致的,现在程序卡死了。
解决方案
顺序上锁。
void thread1() { std::lock_guard<std::mutex> lock1(mutex1); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟一些操作 std::lock_guard<std::mutex> lock2(mutex2); std::cout << "Thread 1 finished" << std::endl; } void thread2() { std::lock_guard<std::mutex> lock2(mutex2); std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟一些操作 std::lock_guard<std::mutex> lock1(mutex1); std::cout << "Thread 2 finished" << std::endl; }
你看,这是资源锁获取顺序不一致导致的。调成一致,就可以有效的回避死锁(必须按照顺序请求资源),但麻烦之处在于,我们就丧失了一定的灵活性!所以,当资源的请求顺序是无所谓的时候,可以采用这个办法。但是,效率必然不会高!
另一个办法是——优化自己的编程思路,看看是不是真的不得不用多重锁,如果优化到后面,发现可以使用其他方案解决,那就更好了。
超时机制
还有一种,如果你的上锁是比较宽松的,允许获取锁失败,可以尝试使用超时集制:
std::timed_mutex mutex1; std::timed_mutex mutex2; void thread1() { auto start = std::chrono::steady_clock::now(); while (true) { // 愿意花费500ms时间等待 if (mutex1.try_lock_for(std::chrono::milliseconds(100))) { // 愿意花费500ms时间等待 if (mutex2.try_lock_for(std::chrono::milliseconds(100))) { // works... std::cout << "Thread 1 finished" << std::endl; mutex2.unlock(); mutex1.unlock(); return; } mutex1.unlock(); } // 如果超过1s没有拿到锁,放弃,退到后面我们处理 if (std::chrono::steady_clock::now() - start > std::chrono::seconds(1)) { std::cout << "Thread 1 timeout" << std::endl; return; } } }
使用RAII来保证锁的恰当释放
哎呀!很不小心的,我们写下了这段代码:
static int shared_value; std::mutex mtx; void worker() { for (int i = 0; i < 1000000; i++) { mtx.lock(); shared_value++; } } int main() { auto thread1 = std::thread(worker); auto thread2 = std::thread(worker); thread1.join(); thread2.join(); std::print("value of shared is: {}\n", shared_value); }
你看到问题了嘛?答案是——我们的锁忘记解锁了,再下一轮循环中,请求一个永远不可能解锁的锁!因为锁了锁的人是过去的他自己!
麻烦了,这个倒还好,如果我们的逻辑非常复杂,如何保证我们自己还记得释放锁呢?答案是请出来万能的RAII机制。
/** @brief A simple scoped lock type. * * A lock_guard controls mutex ownership within a scope, releasing * ownership in the destructor. * * @headerfile mutex * @since C++11 */ template<typename _Mutex> class lock_guard { public: typedef _Mutex mutex_type; [[__nodiscard__]] explicit lock_guard(mutex_type& __m) : _M_device(__m) { _M_device.lock(); } [[__nodiscard__]] lock_guard(mutex_type& __m, adopt_lock_t) noexcept : _M_device(__m) { } // calling thread owns mutex ~lock_guard() { _M_device.unlock(); } lock_guard(const lock_guard&) = delete; lock_guard& operator=(const lock_guard&) = delete; private: mutex_type& _M_device; }; /// @} group mutexes _GLIBCXX_END_NAMESPACE_VERSION } // namespace
看懂了嘛?这就是RAII思路设计的lock_guard。就是说,当我们构造了一个类lock_guard开始,我们就对他立马进行上锁,到出作用域,程序马上就要放手没法管住mutex的控制的时候,手动的进行释放处理操作,在这里,释放的操作就是将锁进行解锁(显然我们的锁必然已经被上锁了,因为锁在被lock_guard控制的一瞬间就调用了lock方法!)
但是还是不够灵活,我们如果想自己控制锁的范围,办法是——使用unique_ptr的姊妹类——unique_lock
-
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
进行初始化,并假设该互斥量已经被当前线程成功加锁。