C++并发编程-3.互斥与死锁

  • 参考:https://llfc.club/category?catid=225RaiVNI8pFDD5L4m807g7ZwmF#!aid/2UVOC0CihIdfguQFmv220vs5hAG

简介

  • 本文介绍如何使用互斥量保证共享数据的安全,并讲述死锁的相关处理方案。

使用锁的两种方式

1. 锁的使用(直接使用mutex)

  • 我们可以通过mutex对共享数据进行夹所,防止多线程访问共享区造成数据不一致问题。如下,我们初始化一个共享变量 hared_data,然后定义了一个互斥量std::mutex,接下来启动了两个线程,分别执行use_lock增加数据,和一个lambda表达式减少数据。结果可以看到两个线程对于共享数据的访问是独占的,单位时间片只有一个线程访问并输出日志。

std::chrono::duration

在这里插入图片描述

#include<iostream>
#include<mutex>

using std::cout;
using std::endl;

std::mutex mtx1;
int shared_data = 100;

void use_lock()
{
    while (true)
    {
        mtx1.lock(); // 加锁
        shared_data++;
        cout << "current thread is " << std::this_thread::get_id() << endl;
        cout << "shared data is " << shared_data << endl;
        mtx1.unlock(); // 解锁
        std::this_thread::sleep_for(std::chrono::seconds(4));  // 微妙
    }
}

void test_lock()
{
    std::thread t1(use_lock);

    std::thread t2([]() {
        while (true)
        {
            mtx1.lock();
            shared_data--;
            cout << "current thread is " << std::this_thread::get_id() << endl;
            cout << "shared data is " << shared_data << endl;
            mtx1.unlock();
            std::this_thread::sleep_for(std::chrono::seconds(4));
        }
        });
    t1.join();
    t2.join();
}
int main()
{
    test_lock();
    return 0;
}

在这里插入图片描述

2. lock_guard的使用

  • 当然我们可以用lock_guard自动加锁和解锁,比如上面的函数可以等价简化为
void use_lcok2()
{
    while (true)
    {
        std::lock_guard<std::mutex> lock(mtx1);
        shared_data++;
        cout << "current thread is " << std::this_thread::get_id() << endl;
        cout << "shared data is " << shared_data << endl;
        std::this_thread::sleep_for(std::chrono::seconds(4));
    }
}

lock_guard在作用域结束时自动调用其析构函数解锁,这么做的一个好处是简化了一些特殊情况从函数中返回的写法,比如异常或者条件不满足时,函数内部直接return,锁也会自动解开。

  • 但是上面的也不推荐使用,因为会存在一直不释放锁的危险,抱着锁睡觉,将锁放在局部作用域中。
void use_lcok2()
{
    while (true)
    {
        {
            std::lock_guard<std::mutex> lock(mtx1);
            shared_data++;
            cout << "current thread is " << std::this_thread::get_id() << endl;
            cout << "shared data is " << shared_data << endl;
        }
        std::this_thread::sleep_for(std::chrono::seconds(4));
    }
}

如何保证数据安全

有时候我们可以将对共享数据的访问和修改聚合到一个函数,在函数内加锁保证数据的安全性。但是对于读取类型的操作,即使读取函数是线程安全的,但是返回值抛给外边使用,存在不安全性。比如一个栈对象,我们要保证其在多线程访问的时候是安全的,可以在判断栈是否为空,判断操作内部我们可以加锁,但是判断结束后返回值就不在加锁了,就会存在线程安全问题。比如我定义了如下栈, 对于多线程访问时判断栈是否为空,此后两个线程同时出栈,可能会造成崩溃。

  • 为什么需要mutable关键字,比如,在一个const函数中,我们需要在函数中进行加锁,加锁操作是一个修改操作。

#include<iostream>
#include<mutex>
#include<stack>
using std::cout;
using std::endl;

std::mutex mtx1;
int shared_data = 100;


template<typename T>
class threadSafeStack1
{
private:
    std::stack<T> data;
    mutable std::mutex mtx;
public:
    threadSafeStack1() {}
    threadSafeStack1(const threadSafeStack1& other)
    {
        std::lock_guard<std::mutex> lock(other.mtx);
        // 在构造函数的函数体内进行复制操作
        data = other.data;
    }
    threadSafeStack1& operator=(const threadSafeStack1&) = delete;
    
    void push(T new_value)
    {
        std::lock_guard<std::mutex> lock(mtx);
        data.push(std::move(new_value));
    }

    // 问题代码

    T pop()
    {
        std::lock_guard<std::mutex> lock(mtx);
        auto elem = data.top();
        data.pop();
        return elem;
    }
    // 产生的问题:在t1时刻返回栈的状态(有一个数据),但是这个状态可能过一会采用此时弹栈,栈的状态发生变化(为空)
    bool empty()const
    {
        std::lock_guard<std::mutex> lock(mtx);
        return data.empty();
    }
};
//如下, 线程1和线程2先后判断栈都不为空,之后执行出战操作,会造成崩溃。


void test_threadSafeStack1()
{
    threadSafeStack1<int> safe_stack;
    safe_stack.push(1);

    std::thread t1([&safe_stack]() {
            if (!safe_stack.empty())
            {
                std::this_thread::sleep_for(std::chrono::seconds(1));
                safe_stack.pop();
            }
        });
    std::thread t2([&safe_stack]() {
            if (!safe_stack.empty())
            {
                std::this_thread::sleep_for(std::chrono::seconds(1));
                safe_stack.pop();
            }
        });
    t1.join();
    t2.join();
}

int main()
{
    //test_lock();
    test_threadSafeStack1();
    return 0;
}

在这里插入图片描述

造成的原因:前一秒的栈判为非空要抛出,但是就在这一秒内另一个线程先把1给抛了,然后本线程再执行pop抛就会报错

下面的修改还是会造成问题
在这里插入图片描述
在这里插入图片描述
cout不是线程安全的
在这里插入图片描述在这里插入图片描述

  • 作者的思路
    解决这个问题我们可以用抛出异常的方式,比如定义一个空栈的异常

在这里插入图片描述

 T pop()
 {
     std::lock_guard<std::mutex> lock(mtx);
     if (data.empty())
     {
         throw empty_stack();
     }
     auto element = data.top();
     data.pop();
     /*出现另外一个问题:返回值左拷贝时,程序奔溃会造成数据丢失
     比如T是一个vector<int>类型,那么在pop函数内部element就是vector<int>类型,开始element存储了一些int值,程序没问题,函数执行pop操作, 假设此时程序内存暴增,
     导致当程序使用的内存足够大时,可用的有效空间不够, 函数返回element时,就会就会存在vector做拷贝赋值时造成失败。即使我们捕获异常,释放部分空间但也会导致栈元素已经出栈,数据丢失了。
     这其实是内存管理不当造成的,但是C++ 并发编程一书中给出了优化方案。
     */
     return element;            
 }


在这里插入图片描述

《并发编程实战》一书给出方案:使用智能指针


struct empty_stack : std::exception
{
     const char* what() const throw()
     {
            return "Stack is empty";
      }
};
template<typename T>
class threadsafe_stack
{
private:
    std::stack<T> data;
    mutable std::mutex m;
public:
    threadsafe_stack() {}
    threadsafe_stack(const threadsafe_stack& other)
    {
        std::lock_guard<std::mutex> lock(other.m);
        //①在构造函数的函数体(constructor body)内进行复制操作
        data = other.data;
    }
    threadsafe_stack& operator=(const threadsafe_stack&) = delete;
    void push(T new_value)
    {
        std::lock_guard<std::mutex> lock(m);
        data.push(std::move(new_value));
    }
    std::shared_ptr<T> pop()
    {
        std::lock_guard<std::mutex> lock(m);
        //②试图弹出前检查是否为空栈
        if (data.empty()) throw empty_stack();
        //③改动栈容器前设置返回值
        std::shared_ptr<T> const res(std::make_shared<T>(data.top()));
        data.pop();
        return res;
    }
    void pop(T& value)
    {
        std::lock_guard<std::mutex> lock(m);
        if (data.empty()) throw empty_stack();
        value = data.top();
        data.pop(); 
    }
    bool empty() const
    {
        std::lock_guard<std::mutex> lock(m);
        return data.empty();
    }
};

void test_thread_safe_stack() {
    threadsafe_stack<int> stack;
    stack.push(42);

    std::thread t1([&stack]() {
        try {
            auto val = stack.pop();
            std::cout << "Thread 1 popped: " << *val << '\n';
        }
        catch (const empty_stack& e) {
            std::cout << "Thread 1: " << e.what() << '\n';
        }
        });

    std::thread t2([&stack]() {
        try {
            auto val = stack.pop();
            std::cout << "Thread 2 popped: " << *val << '\n';
        }
        catch (const empty_stack& e) {
            std::cout << "Thread 2: " << e.what() << '\n';
        }
        });

    t1.join();
    t2.join();
}

在这里插入图片描述

死锁怎么造成

  • 死锁一般是由于调运顺序不一致导致的,比如两个线程循环调用。当线程1先加锁A,再加锁B,而线程2先加锁B,再加锁A。那么在某一时刻就可能造成死锁。比如线程1对A已经加锁,线程2对B已经加锁,那么他们都希望彼此占有对方的锁,又不释放自己占有的锁导致了死锁。

下面的代码没有发送死锁

 //死锁没有发送啊
std::mutex  t_lock1;
std::mutex  t_lock2;
int m_1 = 0;
int m_2 = 1;
void dead_lock1() {
    while (true) {
        std::cout << "dead_lock1 begin " << std::endl;
        t_lock1.lock();
        std::this_thread::sleep_for(std::chrono::seconds(2));
        m_1 = 1024;
        t_lock2.lock();
        m_2 = 2048;
        t_lock2.unlock();
        t_lock1.unlock();
        std::cout << "dead_lock2 end " << std::endl;
    }
}
void dead_lock2() {
    while (true) {
        std::cout << "dead_lock2 begin " << std::endl;
        t_lock2.lock();
        std::this_thread::sleep_for(std::chrono::seconds(2));
        m_2 = 2048;
        t_lock1.lock();
        m_1 = 1024;
        t_lock1.unlock();
        t_lock2.unlock();

        std::cout << "dead_lock2 end " << std::endl;
    }
}
void test_dead_lock() {
    std::thread t1(dead_lock1);
    std::thread t2(dead_lock2);
    t1.join();
    t2.join();
}

这样运行之后在某一个时刻一定会导致死锁。
实际工作中避免死锁的一个方式就是将加锁和解锁的功能封装为独立的函数,
这样能保证在独立的函数里执行完操作后就解锁,不会导致一个函数里使用多个锁的情况


//加锁和解锁作为原子操作解耦合,各自只管理自己的功能
std::mutex  t_lock1;
std::mutex  t_lock2;
int m_1 = 0;
int m_2 = 1;
void atomic_lock1() {
    std::cout << "lock1 begin lock" << std::endl;
    t_lock1.lock();
    m_1 = 1024;
    t_lock1.unlock();
    std::cout << "lock1 end lock" << std::endl;
}
void atomic_lock2() {
    std::cout << "lock2 begin lock" << std::endl;
    t_lock2.lock();
    m_2 = 2048;
    t_lock2.unlock();
    std::cout << "lock2 end lock" << std::endl;
}
void safe_lock1() {
    while (true) {
        atomic_lock1();
        atomic_lock2();
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
    }
}
void safe_lock2() {
    while (true) {
        atomic_lock2();
        atomic_lock1();
        std::this_thread::sleep_for(std::chrono::milliseconds(5));
    }
}
void test_safe_lock() {
    std::thread t1(safe_lock1);
    std::thread t2(safe_lock2);
    t1.join();
    t2.join();
}
int main()
{
    //test_lock();
    //test_threadSafeStack1();
    test_safe_lock();
    return 0;
}

同时加锁

当我们无法避免在一个函数内部使用两个互斥量,并且都要解锁的情况,那我们可以采取同时加锁的方式。我们先定义一个类,假设这个类不推荐拷贝构造,但我们也提供了这个类的拷贝构造和移动构造


class som_big_object {
public:
    som_big_object(int data) :_data(data) {}
    //拷贝构造
    som_big_object(const som_big_object& b2) :_data(b2._data) {
    }
    //移动构造
    som_big_object(som_big_object&& b2) :_data(std::move(b2._data)) {
    }
    //重载输出运算符
    friend std::ostream& operator << (std::ostream& os, const som_big_object& big_obj) {
        os << big_obj._data;
        return os;
    }
    //重载赋值运算符
    som_big_object& operator = (const som_big_object& b2) {
        if (this == &b2) {
            return *this;
        }
        _data = b2._data;
        return *this;
    }
    //交换数据
    friend void swap(som_big_object& b1, som_big_object& b2) {
        som_big_object temp = std::move(b1);
        b1 = std::move(b2);
        b2 = std::move(temp);
    }
private:
    int _data;
};
  
//        接下来我们定义一个类对上面的类做管理,为防止多线程情况下数据混乱, 包含了一个互斥量。

class big_object_mgr {
public:
    big_object_mgr(int data = 0) :_obj(data) {}
    void printinfo() {
        std::cout << "current obj data is " << _obj << std::endl;
    }
    friend void danger_swap(big_object_mgr& objm1, big_object_mgr& objm2);
    friend void safe_swap(big_object_mgr& objm1, big_object_mgr& objm2);
    friend void safe_swap_scope(big_object_mgr& objm1, big_object_mgr& objm2);
private:
    std::mutex _mtx;
    som_big_object _obj;
};

//        为了方便演示哪些交换是安全的,哪些是危险的,所以写了三个函数。
void danger_swap(big_object_mgr& objm1, big_object_mgr& objm2) {
    std::cout << "thread [ " << std::this_thread::get_id() << " ] begin" << std::endl;
    if (&objm1 == &objm2) {
        return;
    }
    std::lock_guard <std::mutex> gurad1(objm1._mtx);
    //此处为了故意制造死锁,我们让线程小睡一会
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::lock_guard<std::mutex> guard2(objm2._mtx);
    swap(objm1._obj, objm2._obj);
    std::cout << "thread [ " << std::this_thread::get_id() << " ] end" << std::endl;
}
//danger_swap是危险的交换方式。比如如下调用
void  test_danger_swap() {
    big_object_mgr objm1(5);
    big_object_mgr objm2(100);
    std::thread t1(danger_swap, std::ref(objm1), std::ref(objm2)); // 注意这里使用的是ref,因为线程函数的参数是引用。
    std::thread t2(danger_swap, std::ref(objm2), std::ref(objm1));
    t1.join();
    t2.join();
    objm1.printinfo();
    objm2.printinfo();
}

这种调用方式存在隐患,因为danger_swap函数在两个线程中使用会造成互相竞争加锁的情况。那就需要用锁同时锁住两个锁。


void safe_swap(big_object_mgr& objm1, big_object_mgr& objm2) {
    std::cout << "thread [ " << std::this_thread::get_id() << " ] begin" << std::endl;
    if (&objm1 == &objm2) {
        return;
    }
    // 同时加锁
    std::lock(objm1._mtx, objm2._mtx);
    //领养锁管理它自动释放,gurad1本来既要加锁又要解锁,因为是adopt_lock领养的,所以只需要解锁,不需要加锁
    std::lock_guard <std::mutex> gurad1(objm1._mtx, std::adopt_lock);
    //此处为了故意制造死锁,我们让线程小睡一会
    std::this_thread::sleep_for(std::chrono::seconds(1));
    std::lock_guard <std::mutex> gurad2(objm2._mtx, std::adopt_lock);
    

    swap(objm1._obj, objm2._obj);
    std::cout << "thread [ " << std::this_thread::get_id() << " ] end" << std::endl;
}

void test_safe_swap() {
    big_object_mgr objm1(5);
    big_object_mgr objm2(100);
    std::thread t1(safe_swap, std::ref(objm1), std::ref(objm2));
    std::thread t2(safe_swap, std::ref(objm2), std::ref(objm1));
    t1.join();
    t2.join();
    objm1.printinfo();
    objm2.printinfo();
}

在这里插入图片描述

  • 切换标准
    在这里插入图片描述

  • 当然上面加锁的方式可以简化,C++17 scope_lock可以对多个互斥量同时加锁,并且自动释放

//上述代码可以简化为以下方式
void safe_swap_scope(big_object_mgr& objm1, big_object_mgr& objm2) {
    std::cout << "thread [ " << std::this_thread::get_id() << " ] begin" << std::endl;
    if (&objm1 == &objm2) {
        return;
    }
    std::scoped_lock  guard(objm1._mtx, objm2._mtx);
    //等价于
    //std::scoped_lock<std::mutex, std::mutex> guard(objm1._mtx, objm2._mtx);
    swap(objm1._obj, objm2._obj);
    std::cout << "thread [ " << std::this_thread::get_id() << " ] end" << std::endl;
}

层级锁

现实开发中常常很难规避同一个函数内部加多个锁的情况,我们要尽可能避免循环加锁,所以可以自定义一个层级锁,保证实际项目中对多个互斥量加锁时是有序的。
在这里插入图片描述

class hierarchical_mutex {
public:
    explicit hierarchical_mutex(unsigned long value) :_hierarchy_value(value),
        _previous_hierarchy_value(0) {}
    hierarchical_mutex(const hierarchical_mutex&) = delete;
    hierarchical_mutex& operator=(const hierarchical_mutex&) = delete;
    void lock() {
        check_for_hierarchy_violation();
        _internal_mutex.lock();
        update_hierarchy_value();
    }
    void unlock() {
        if (_this_thread_hierarchy_value != _hierarchy_value) {
            throw std::logic_error("mutex hierarchy violated");
        }
        _this_thread_hierarchy_value = _previous_hierarchy_value;
        _internal_mutex.unlock();
    }
    bool try_lock() {
        check_for_hierarchy_violation();
        if (!_internal_mutex.try_lock()) {
            return false;
        }
        update_hierarchy_value();
        return true;
    }
private:
    std::mutex  _internal_mutex;
    //当前层级值
    unsigned long const _hierarchy_value;
    //上一次层级值
    unsigned long _previous_hierarchy_value;
    //本线程记录的层级值
    static thread_local  unsigned long  _this_thread_hierarchy_value;
    void check_for_hierarchy_violation() {
        if (_this_thread_hierarchy_value <= _hierarchy_value) {
            throw  std::logic_error("mutex  hierarchy violated");
        }
    }
    void  update_hierarchy_value() {
        _previous_hierarchy_value = _this_thread_hierarchy_value;
        _this_thread_hierarchy_value = _hierarchy_value;
    }
};
thread_local unsigned long hierarchical_mutex::_this_thread_hierarchy_value(ULONG_MAX);
void test_hierarchy_lock() {
    hierarchical_mutex  hmtx1(1000);
    hierarchical_mutex  hmtx2(500);
    std::thread t1([&hmtx1, &hmtx2]() {
        hmtx1.lock();
        hmtx2.lock();
        hmtx2.unlock();
        hmtx1.unlock();
        });
    std::thread t2([&hmtx1, &hmtx2]() {
        hmtx2.lock();
        hmtx1.lock();
        hmtx1.unlock();
        hmtx2.unlock();
        });
    t1.join();
    t2.join();
}

层级锁能保证我们每个线程加锁时,一定是先加权重高的锁。并且释放时也保证了顺序。主要原理就是将当前锁的权重保存在线程变量中,这样该线程再次加锁时判断线程变量的权重和锁的权重是否大于,如果满足条件则继续加锁

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值