可重入锁与不可重入锁以及常见的死锁现象

可重入锁和不可重入锁

Mutex可以分为递归锁(recursive mutex)和非递归锁(non-recursive mutex)。

  • 可递归锁也可称为可重入锁(reentrant mutex)
  • 非递归锁又叫不可重入锁(non-reentrant mutex)
  • 递归锁具有线程排他性
    二者唯一的区别是,同一个线程可以多次获取同一个递归锁,不会产生死锁。而如果一个线程多次获取同一个非递归锁,则会产生死锁。

Windows下的Mutex和Critical Section是可递归的。Linux下的pthread_mutex_t锁默认是非递归的。可以显示的设置PTHREAD_MUTEX_RECURSIVE属性,将pthread_mutex_t设为递归锁。

死锁

当两个任务都在等待被对方持有的资源时,两个任务都无法再继续执行,这种情况就被称为死锁。
​​在这里插入图片描述
■ 死锁情况1:
eg1: 滥用锁导致

void foo()
{
	mutex.lock();
	// do something
	mutex.unlock();
}

void bar()
{
	mutex.lock();
	// do something
	foo();
	mutex.unlock();	
}

foo函数和bar函数都获取了同一个锁,而bar函数又会调用foo函数。如果MutexLock锁是个非递归锁,则这个程序会立即死锁。因此在为一段程序加锁时要格外小心,否则很容易因为这种调用关系而造成死锁。

原则上不应该出现这样的代码设计,不推崇在互斥锁保护的区域使用用户自定义的代码。解决方案:

  1. 即使出现了这样的情况,可以使用c++11推出的recursive_mutex(递归锁)来解决或者系统提供的递归锁能力。
  2. 新增一个不加锁版本 foo_without_lock()

■ 死锁情况2:

Thread A              Thread B
_mu.lock()            _mu2.lock()
//死锁             //死锁
_mu2.lock()           _mu.lock()

避免死锁

避免死锁,有以下几点建议:

  1. 建议尽量同时只对一个互斥锁上锁。
{
    std::lock_guard<std::mutex> guard(_mu2);
    //do something
    f << msg << id << endl;
}
{
    std::lock_guard<std::mutex> guard2(_mu);
    cout << msg << id << endl;
}
  1. 不要在互斥锁保护的区域使用用户自定义的代码,因为用户的代码可能操作了其他的互斥锁。
{
    std::lock_guard<std::mutex> guard(_mu2);
    user_function(); // never do this!!!
    f << msg << id << endl;
}
  1. 如果想同时对多个互斥锁上锁,推荐使用std::scoped_lock(C++17)。
  • 类 scoped_lock 是提供便利 RAII 风格机制的互斥包装器,它在作用域块的存在期间占有一或多个互斥。

  • 创建 scoped_lock 对象时,它试图取得给定互斥的所有权。控制离开创建 scoped_lock 对象的作用域时,析构 scoped_lock 并以逆序释放互斥。若给出数个互斥,则使用免死锁算法,如同以 std::lock 。

  • scoped_lock 类不可复制。
    eg1:

#include <mutex>
#include <thread>
#include <iostream>
#include <vector>
#include <functional>
#include <chrono>
#include <string>
 
struct Employee {
    Employee(std::string id) : id(id) {}
    std::string id;
    std::vector<std::string> lunch_partners;
    std::mutex m;
    std::string output() const
    {
        std::string ret = "Employee " + id + " has lunch partners: ";
        for( const auto& partner : lunch_partners )
            ret += partner + " ";
        return ret;
    }
};
 
void send_mail(Employee &, Employee &)
{
    // 模拟耗时的发信操作
    std::this_thread::sleep_for(std::chrono::seconds(1));
}
 
void assign_lunch_partner(Employee &e1, Employee &e2)
{
    static std::mutex io_mutex;
    {
        std::lock_guard<std::mutex> lk(io_mutex);
        std::cout << e1.id << " and " << e2.id << " are waiting for locks" << std::endl;
    }
 
    {
        // 用 std::scoped_lock 取得二个锁,而无需担心
        // 其他对 assign_lunch_partner 的调用死锁我们
        // 而且它亦提供便利的 RAII 风格机制
 
        std::scoped_lock lock(e1.m, e2.m);
 
        // 等价代码 1 (用 std::lock 和 std::lock_guard )
        // std::lock(e1.m, e2.m);
        // std::lock_guard<std::mutex> lk1(e1.m, std::adopt_lock);
        // std::lock_guard<std::mutex> lk2(e2.m, std::adopt_lock);
 
        // 等价代码 2 (若需要 unique_lock ,例如对于条件变量)
        // std::unique_lock<std::mutex> lk1(e1.m, std::defer_lock);
        // std::unique_lock<std::mutex> lk2(e2.m, std::defer_lock);
        // std::lock(lk1, lk2);
        {
        	// 因为io流也不是线程安全的,因此也要加锁
            std::lock_guard<std::mutex> lk(io_mutex);
            std::cout << e1.id << " and " << e2.id << " got locks" << std::endl;
        }
        e1.lunch_partners.push_back(e2.id);
        e2.lunch_partners.push_back(e1.id);
    }
 
    send_mail(e1, e2);
    send_mail(e2, e1);
}
 
int main()
{
    Employee alice("alice"), bob("bob"), christina("christina"), dave("dave");
 
    // 在并行线程中指派,因为就午餐指派发邮件消耗很长时间
    std::vector<std::thread> threads;
    threads.emplace_back(assign_lunch_partner, std::ref(alice), std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(bob));
    threads.emplace_back(assign_lunch_partner, std::ref(christina), std::ref(alice));
    threads.emplace_back(assign_lunch_partner, std::ref(dave), std::ref(bob));
 
    for (auto &thread : threads) thread.join();
    std::cout << alice.output() << '\n'  << bob.output() << '\n'
              << christina.output() << '\n' << dave.output() << '\n';
}

参考文章:
[c++11]多线程编程(四)——死锁(Dead Lock)
线程同步之利器(1)——可递归锁与非递归锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值