首先文中提出一个概念: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++用来数据保护的有效工具,但是他不是万能的。
- 你需要好好设计你的代码,正确的代码保护正确的数据;demo1
- 避免接口造成的 race condition; demo2
- 使用锁,也会随之带来死锁的风险;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() 不可信导致的。具体情况为
上面情况为什么会发生 race condition?
假设stack中只有一个元素,两个线程同时运行上述代码。thread2 先将最后一个元素删除掉了,这时候 thread1 再去运行 top(),就会发生未定义行为。
按照如上设计的接口,虽然每个接口中的内容都被锁保护起来,但是上述代码仍然不是线程安全的。(一个空 stack 对象调用 top() 是一个未定义的行为)。该 race condition 是由于接口设计不规范导致的。那如何修改呢?
书中提出,最简单的方法就是 对 top() 函数的调用 throw exception。我理解这种方法实际上并没解决 race condition,而是当发生race condition 问题后,如何避免对代码运行造成伤害。
其实细心的读者会发现,接口造成的race condition 不光会出现在 empty() 和 top() 函数之间。还会发生在top() 和 pop() 之间。
按照上述代码,如果两个线程会拿到统一 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();
}
};
复制代码