《C++ Concurrency IN ACTION》chapter 3 线程之间数据共享

首先文中提出一个概念:invariants-statements(语义不变性,我是这样翻译的哈哈哈),那么什么是 invariants-statements 呢?

举一个例子,双向链表:Z<=>A<=>B<=>C ,其中的语义不变性就是 A 的下一个节点是B,B的上一个节点是A,这条语义对于任何线程都是一样的。当你要删除节点B的时候:1. A->next = C; 2. C->previous = A;3. delete B

当thread1删除节点B的时候,其他线程不可以看到这系列操作的中间状态。

我们先列出本章节的目的是解决,多线程处理共享数据可能会遇到的两种问题:

  • race condition
  • 死锁

如何加锁

如果你将所有的访问共享变量的代码都标记为互斥,这样也不可取的,因为这样同一时间,只会有一个线程会访问共享数据,其他线程只能等待。

锁是C++用来数据保护的有效工具,但是他不是万能的。

  1. 你需要好好设计你的代码,正确的代码保护正确的数据;demo1
  2. 避免接口造成的 race condition; demo2
  3. 使用锁,也会随之带来死锁的风险;demo3

锁和共享数据应该被包装成类

#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();
}
复制代码

看代码,共享变量为 some_list,有一个全局锁 some_mutex,只有两个接口去操作 some_list。每个接口入口处获取锁,离开接口时释放锁。因此在该demo中,多个线程中,只有一个线程会去访问共享变量 some_list。

这里有一个违背使用方法的:在C++ 被保护的数据 和 锁,应该封装在一个类中,而不应该直接像这样初始化为全局对象。

1 好好设计你的代码,正确的代码保护正确的数据

什么叫做好好设计代码,什么样的设计算好的呢?让我们看下面的例子

demo1,代码如下:

class some_data {
    int a;
    std::string b;
public:
    void do_something();
};

calss 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 = &proctected_data;
}

void foo() {
    x.process_data(malicious_function);
    unprotected->do_something();
}
复制代码

这段代码存在一个最大的风险:由于传入用户自定义函数指针,将受保护的共享数据的地址传递到了锁的外面,这样共享书就不再受锁的保护,会被随机更改。

demo1 给到我们的指南就是:

不要将共享数据的指针或引用传递到锁范围之外。即不要将他们做为函数的返回、存储他们到外部可见的内存中、传递他们到用户自定义的函数中。

2. race condition

本章节会介绍:

  • 什么是 race condition
  • 如何解决 race condition: 加锁
  • 如何解决造成的 race condition?

race condition 我理解就是多线程相互竞争访问共享数据(一个或多个),造成某个线程访问到共享数据的中间状态(或语义被破坏时的状态)。

problematic race condition 发生在某个操纵需要修改两个或多个共享数据的时候。以上述双向链表为例,一个线程需要删除节点B,那么就必须访问两个共享数据:A 和 C,当该线程刚修改完节点A-next,这时候另外一个线程遍历双向链表的时候就会出现 race condition。

2.1 race condition 如何发生

接口之间会相互影响,造成 race condition。那么接口之间如何作用,才会造成 race condition呢??? demo2 会详细介绍接口如何造成 race condition。

demo2,代码如下:

template<typename T,typename Container=std::deque<T> >
class stack { 
public:
    explicit stack(const Container&);
    explicit stack(Container&& = Container());
    template <class Alloc> explicit stack(const Alloc&); 
    template <class Alloc> stack(const Container&, const Alloc&); 
    template <class Alloc> stack(Container&&, const Alloc&); 
    template <class Alloc> stack(stack&&, const Alloc&);
    bool empty() const; size_t size() const; T& top(); T const& top() const; 
    void push(T const&); 
    void push(T&&); 
    void pop(); 
    void swap(stack&&);
};
复制代码

首先第一个可能会遇到的 race condition 是 empty() 和 size() 不可信导致的。具体情况为

图1

上面情况为什么会发生 race condition?

假设stack中只有一个元素,两个线程同时运行上述代码。thread2 先将最后一个元素删除掉了,这时候 thread1 再去运行 top(),就会发生未定义行为

按照如上设计的接口,虽然每个接口中的内容都被锁保护起来,但是上述代码仍然不是线程安全的。(一个空 stack 对象调用 top() 是一个未定义的行为)。该 race condition 是由于接口设计不规范导致的。那如何修改呢?

书中提出,最简单的方法就是 对 top() 函数的调用 throw exception。我理解这种方法实际上并没解决 race condition,而是当发生race condition 问题后,如何避免对代码运行造成伤害。

其实细心的读者会发现,接口造成的race condition 不光会出现在 empty() 和 top() 函数之间。还会发生在top() 和 pop() 之间。

image.png

按照上述代码,如果两个线程会拿到统一 value,但是运行 pop() 函数,会删除不同的元素,我想这一定不是用户想要的运行结果。

tips: 为什么stack pop()的返回是void而不是 元素类型:因为如果 函数是 T pop() {} 的形式,在用户调用的时候,很有可能在拷贝复制return 结果的时候,内存不足,造成 元素已经从stack 中删除了,但是用户并没有拿到该元素的值; 也正是这种分离设计,造成了上面接口导致的 race condition 局面

2.2 如果解决接口之间造成的 race condition呢?

这里的解决办法很好理解,直接将 top() 函数 和 pop() 函数合二为一。

#include <exception> 
#include <memory>

struct empty_stack: std::exception { 
    const char* what() const throw(); 
};

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);
        data=other.data;
    }
    threadsafe_stack& operator=(const threadsafe_stack&) = delete;
    
    void push(T new_value) {
        std::lock_guard<std::mutex> lock(m);
        data.push(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(); 
    }
};
复制代码

猜你喜欢

转载自juejin.im/post/7110500674265317407