C++11使用互斥量保护共享数据

C++中使用互斥量

在C++11中,可以通过实例化std::mutex创建互斥量,可以通过调用成员函数lock()进行上锁,调用unlock()进行解锁。

例如:

int             g_num  =  0;
std::mutex      g_num_mutex;

void slow_increment(int id)
{
    for (int i = 0; i < 3; ++i)
    {
        //加锁
        g_num_mutex.lock();
        ++g_num;
        std::cout << id << " => " << g_num << '\n';
        //若在这期间,发生异常,将导致锁无法释放,异常
        //解锁
        g_num_mutex.unlock();
        
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
}

int _tmain(int argc, _TCHAR* argv[])
{
    std::thread td1(slow_increment, 0);
    std::thread td2(slow_increment, 0);
    td1.join();
    td2.join();
 }

不过,不推荐实践中直接去调用成员函数,因为调用成员函数就意味着,必须记住在每个函数出口都要去调用unlock(),也包括异常的情况,如果处理不当,将导致锁无法释放,程序卡住。

因此,C++库为互斥量提供了一个RAII语法的模板类std::lock_guard,其会在构造的时候提供已锁的互斥量,并在析构的时候进行解锁,从而保证了一个已锁的互斥量总是会被正确的解锁。对于以上的代码,我们只需要将全局变量用lock_guard锁即可大大提高程序的健壮性。

修改代码如下:

for (int i = 0; i < 3; ++i)
{
    //使用RAII语法,提高程序的健壮性
    std::lock_guard<std::mutex> gurad(g_num_mutex);
    ++g_num;
    ...
}
基于面向对象准则,保护共享数据

虽然某些情况下,使用全局变量没问题,但在大多数情况下,互斥量通常会与保护的数据放在同一个类中,而不是定义成全局变量。这是面向对象设计的准则:将互斥量放在一个类中,对类的功能进行封装,并进行数据保护。在这种情况下,互斥量和要保护的数据都被定义成private成员,这会让访问数据的代码变的清晰

可靠的代码设计

进行代码设计时,一定要特别注意不要返回受保护数据的指针或者引用,否则数据安全是不可以靠的,例如:

template<typename T>
class CThreadsafeStack
{
public:
    //不要设计这样的接口,是不安全行为
    //因为用户可以在类外修改数据
    std::stack<T>& GetData()
    {
        std::lock_guard<std::mutex> guard(m_mutex);
        return m_data;
    }

private:
    std::mutex m_mutex;  //互斥量
    std::stack<T> m_data;//受保护的数据
};

接口内在的条件竞争

在C++ STL模板库中,已经实现stack功能,但是std::stack<>却不是线程安全的,因为stack成员函数存在恶性条件的数据竞争,导致多线程环境下,数据出现错误。

例如:

std::stack<int> st;
st.push(100);
st.push(200);
st.push(300);

std::thread td1([&](){
    int value = st.top(); 
    //其他操作
    std::this_thread::sleep_for(std::chrono::milliseconds(5));
    st.pop();
    //自己实现的打印函数,保证打印互斥,
    //可以用普通的printf函数实现,只是打印可能不完整
    safe_print(value);
});

std::thread td2([&](){
    int value = st.top();
   //其他操作
    std::this_thread::sleep_for(std::chrono::milliseconds(5));
    st.pop();
    //自己实现的打印函数,保证打印互斥。
    safe_print(value);
}); 

td1.join();
td2.join();

运行结果:

get top value is :300
get top value is :300

由于接口之间没有锁机制或者锁的范围太小,导致两个线程对同一个数据进行两次读取(都pop之前进行top操作),导致进行数据处理的时候就出现异常情况(一个数据处理两次)。
在这里插入图片描述

从以上实验以及线程时间片分析,top()和pop()之间存在恶性条件竞争,因为锁的粒度太小(接口内有锁或者干脆没有锁机制),需要保护的操作并未全覆盖到导致。

如果需要达到预期效果,需要有这么一个锁,它能锁住top和pop两个操作才可以,这样锁的粒度就变大了,从设计上来说,并不合理。因为如果一个系统中锁的粒度太大,一个线程需要等待较长时间,导致系统的并发性能就受到了限制。因此原有的stack提供的接口在多线程环境中,显得并不是那么好。

为了达到线程安全以及符合栈的基本操作,需要重新设计线程安全的栈类。
代码如下:

struct empty_stack : std::exception
{
    const char* what() const throw() 
    {
        return "empty stack!";
    };
};

template<typename T>
class CThreadsafeStack
{
public:
    CThreadsafeStack(){}
    CThreadsafeStack(const CThreadsafeStack& other)
    {
        std::lock_guard<std::mutex>(other.m_mutex);
        m_data = other.m_data;
    } 

    //不支持赋值运算
    CThreadsafeStack& operator=(const CThreadsafeStack&) = delete;

    //修改接口以规避接口之间的数据竞争(在pop里面完成top和pop)
    std::shared_ptr<T> pop()
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        // 在调用pop前,检查栈是否为空
        if (m_data.empty()) throw empty_stack(); 
        
        // 在修改堆栈前,分配出返回值
        std::shared_ptr<T> const res(std::make_shared<T>(m_data.top())); 
        m_data.pop();
        return res;
    }

    void pop(T& value)
    {
        std::lock_guard<std::mutex> guard(m_mutex);
        if (m_data.empty())   throw empty_stack();
  
        value = m_data.top();
        m_data.pop();
    }

    void push(T new_value)
    {
        std::lock_guard<std::mutex> guard(m_mutex);
        m_data.push(new_value);
    }

    bool empty() const
    {
        std::lock_guard<std::mutex> guard(m_mutex);
        return m_data.empty();
    }

    std::size_t size() const//const类型
    {
        std::lock_guard<std::mutex> guard(m_mutex);
        return m_data.size();
    }

private:
    mutable std::mutex m_mutex;  //lock_guard传递的是引用类型
    std::stack<T> m_data;
};

测试代码:

CThreadsafeStack <int> st;
//仅一个数据
st.push(1);
std::function <void (int)> func =  [&](int timeout)
{
    try
    {
        if (!st.empty())
        {
            std::this_thread::sleep_for(std::chrono::milliseconds(timeout));
            int nValue = 0;
            st.pop(nValue);
            safe_print(nValue);
        }
    }
    catch (empty_stack &e)
    {
        std::cout << e.what() << std::endl;
    }
};

std::thread td1(func,2);
std::thread td2(func,3);

td1.join();
td2.join();

运行结果:

//弹出一个数据,另外一个报异常 符合预期
get value is :1
empty stack!.
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值