C++11_lock_guard的线程死锁问题和解决

视频教程:https://www.bilibili.com/video/av92453755

std::lock_guard

  • std::lock_guard严格基于作用域(scope-based)的锁管理类模板,构造时是否加锁是可选的(不加锁时假定当前线程已经获得锁的所有权),析构时自动释放锁,所有权不可转移,对象生存期内不允许手动加锁和释放锁。
  • 默认构造函数里锁定互斥量,即调用互斥量的lock函数。
  • 析构函数里解锁互斥量,即调用互斥量的unlock函数。
  • std::lock_guard 对象并不负责管理 Mutex 对象的生命周期,lock_guard 只是简化了 Mutex 对象的上锁和解锁操作,方便线程对互斥量上锁,即在某个 lock_guard对象的声明周期内,它所管理的锁对象会一直保持上锁状态;而 lock_guard的生命周期结束之后,它所管理的锁对象会被解锁

std::unique_lock

  • 与std:::lock_gurad基本一致,但更加灵活的锁管理类模板,构造时是否加锁是可选的,在对象析构时如果持有锁会自动释放锁,所有权可以转移。对象生命期内允许手动加锁和释放锁。但提供了更好的上锁和解锁控制尤其是在程序抛出异常后先前已被上锁的 Mutex 对象可以正确进行解锁操作,极大地简化了程序员编写与 Mutex 相关的异常处理代码****。
  • std::unique_lock的上锁/解锁操作:lock,try_lock,try_lock_for,try_lock_until 和unlock

两者区别

  • std::unique_lock更灵活,提供了lock, unlock, try_lock等接口
  • std::lock_guard更简单,没有多余的接口,构造函数时拿到锁,析构函数时释放锁,但更省时

线程死锁

因为std::lock_guard更简单的特性,所以可能出现下列情况的线程死锁

#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
std::mutex mt1;
std::mutex mt2;
void deadLock(std::mutex& mtA, std::mutex& mtB)
{

    std::lock_guard<std::mutex>lock1(mtA);
    std::cout << "get the first mutex" << " in thread " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(1));

    std::lock_guard<std::mutex>lock2(mtB);
    std::cout << "get the second mutex" << " in thread " << std::this_thread::get_id() << std::endl;


    std::cout << "do something in thread " << std::this_thread::get_id() << std::endl;
}


int main() {
    std::thread t1([&] {deadLock(mt1, mt2); });
    std::thread t2([&] {deadLock(mt2, mt1); });
    t1.join();
    t2.join();
}

解决方式:使用锁定策略+原子锁方式来防止死锁情况

方法一:先同时lock掉两个锁,再构建std::lock_guard

构建lock_guard时,Tag 参数为 std::adopt_lock,表明当前线程已经获得了锁,不需要再锁了,此后mt对象的解锁操作交由 lock_guard 对象 guard 来管理,在 guard 的生命周期结束之后,mt对象会自动解锁。

#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
#include <assert.h>
std::mutex mt1;
std::mutex mt2
void deadLockProcess1(std::mutex& mtA, std::mutex& mtB)
{
    std::lock(mtA, mtB);

    std::lock_guard<std::mutex>lock1(mtA, std::adopt_lock);
    std::cout << "get the first mutex" << " in thread " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(1));

    std::lock_guard<std::mutex>lock2(mtB, std::adopt_lock);
    std::cout << "get the second mutex" << " in thread " << std::this_thread::get_id() << std::endl;
    std::cout << "do something in thread " << std::this_thread::get_id() << std::endl;
}

int main() {
    std::thread t1([&] {deadLockProcess1(mt1, mt2); });
    std::thread t2([&] {deadLockProcess1(mt2, mt1); });
    t1.join();
    t2.join();
}

上述例子使用std::unique_lock其实也可以,但这时使用如上面的lock_guard效率更高。

方法二:先构建std::unique_lock,再同时lock掉两个锁

构建unique_lock时,Tag 参数为 std::defer_lock,表明不lock锁,在执行功能代码之前再统一lock掉两个unique_lock,在 unique_lock 的生命周期结束之后,mt对象自动解锁。

#include <iostream>
#include <chrono>
#include <mutex>
#include <thread>
#include <assert.h>
std::mutex mt1;
std::mutex mt2;
void deadLockProcess2(std::mutex& mtA, std::mutex& mtB)
{
    std::unique_lock<std::mutex>lock1(mtA, std::defer_lock);
    std::cout << "get the first mutex" << " in thread " << std::this_thread::get_id() << std::endl;
    std::this_thread::sleep_for(std::chrono::milliseconds(1));

    std::unique_lock<std::mutex>lock2(mtB, std::defer_lock);
    std::cout << "get the second mutex" << " in thread " << std::this_thread::get_id() << std::endl;

    std::lock(lock1, lock2);

    assert(lock1.owns_lock() == true);


    std::cout << "do something in thread " << std::this_thread::get_id() << std::endl;
}

int main() {
    std::thread t1([&] {deadLockProcess2(mt1, mt2); });
    std::thread t2([&] {deadLockProcess2(mt2, mt1); });
    t1.join();
    t2.join();
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容