C++-多线程数据共享问题和互斥锁

线程数据共享问题

多线程的优势之一就是线程之间可以共享数据,但我们需要一套规则规定哪个线程可以访问哪部分数据,什么时候可以访问,以及怎么通知其他关心该数据的线程已经更新了数据,如果不能处理好数据共享的问题,多线程的这个优势也会变为劣势。
线程间共享数据的所有问题都是因为对数据的修改,一个只读的数据不会造成任何问题。

不变式与竞争条件

这里要提到一个概念:不变式(invariant),意思是关于特定数据结构的陈述始终正确。例如:“x变量保存着列表中元素的个数”,但这些不变式常常在数据更新的时候被破坏。

考虑一个双向链表,节点保存着指向上一个和下一个节点的指针,这里的不变式之一就是”如果你沿着A节点的下一个指针找到了B节点,那么通过B节点的上一个指针也能找到A“,当我们执行删除节点操作时,经历以下步骤:

  1. 确定删除的节点
  2. 更新删除节点的上一节点的下一个指针
  3. 更新删除节点的下一个节点的上一个指针
  4. 删除节点
    在2和3步骤,这个不变式就会破坏(此时关于该数据结构的陈述是错误的),直到删除操作完成时才为真。
    假如现在有三个节点a,b,c,当线程在删除b节点时执行完步骤2,此时另一个线程在执行删除c节点的操作,此时c通过指针找到的上一个节点是b,在访问和修改b节点指针或其他相关数据的时候,b节点可能已经被删除,从而导致程序崩溃。
    这是竞争条件(race condition)的一个典型例子,在并发领域中,竞争条件的含义是结果依赖于线程执行的顺序,有时这些不同的结果都可以接受,但有时这些结果可能破坏不变式,那么就会造成问题。我们说的竞争条件通常是后者。竞争条件可以细分很多种,如数据竞争(多个线程对同一对象的修改)。
    竞争条件经常出现在执行一个操作,该操作包含修改多个独立数据的情况下,当部分数据被修改,另一个线程可能会访问该数据。
    竞争条件通常和比较难以发现和复现,在调试环境下由于改变了程序的执行速度,因此竞争条件通常没法调试中复现。

使用并发技术的编写程序的复杂性主要来自于避免竞争条件

避免竞争条件

解决竞争条件的最简单方法就是把数据通过某种保护机制保护起来,保证只有正在修改数据的线程可以看到操作的中间过程(2,3步骤)。
另一种方式是改变数据结构设计和不变式,使得每次修改数据都是不可再分的操作,写每次修改都能保持不变式,这通常意味着lock-free programming(后面介绍)。
还有一种方式是把修改数据看作事务,即software transactional memory,有兴趣的自行了解一下。

互斥锁

C++标准库提供了std::mutex,有一个lock和unlock方法,但我们通常不直接调用这两个方法,而是使用std::lock_guard,其实现了互斥锁的RAII,构建时锁定互斥锁,析构时解锁互斥锁

#include <list>
#include <mutex>
#include <algorithm>
std::list<int> some_list; 
std::mutex some_mutex; 
void add_to_list(int new_value)
{
    std::lock_guard<std::mutex> guard(some_mutex); 
    some_list.push_back(new_value);
}
bool list_contains(int value_to_find) 
{
    std::lock_guard<std::mutex> guard(some_mutex); 
    return std::find(some_list.begin(),some_list.end(),value_to_find)
    != some_list.end();
}

当有互斥锁已经被一个线程获取,其他线程执行到std::lock_guard<std::mutex> guard(some_mutex);或者some_mutex.lock()时,线程会阻塞在该语句,直到锁被释放。注意,同一线程不能多次获取同一个锁,否则会引发异常。
上面的代码保证了add_to_list和list_contains调用总是互斥的,通常情况下,这足以保护数据的安全,但是要注意,如果接口返回值或者参数包含了相关数据的指针或者引用,就能做到在没有互斥锁保护的情况下随意访问数据,因此这依赖于接口的设计。
除此以外,也不要使用用户的可调用对象来处理数据。如下代码足以说明这一点

class some_data
{
    int a;
    std::string b;

public:
    void do_something();
};
class data_wrapper
{
private:
    some_data data;
    std::mutex m;

public:
    template <typename Function>
    void process_data(Function func)
    {
        std::lock_guard<std::mutex> l(m);
        func(data);//危险!
    }
};
some_data *unprotected;
void malicious_function(some_data &protected_data)//被保护数据的引用
{
    unprotected = &protected_data;//保存数据地址
}
data_wrapper x;
void foo()
{
    x.process_data(malicious_function);
    unprotected->do_something();//随意访问
}

总之,不要将受保护数据的指针和引用传递到锁保护范围之外的,无论是通过从函数返回值、还是将其存储在外部可见的内存中,还是将其作为参数传递给用户提供的可调用对象中。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

mrbone11

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值