线程数据共享问题
多线程的优势之一就是线程之间可以共享数据,但我们需要一套规则规定哪个线程可以访问哪部分数据,什么时候可以访问,以及怎么通知其他关心该数据的线程已经更新了数据,如果不能处理好数据共享的问题,多线程的这个优势也会变为劣势。
线程间共享数据的所有问题都是因为对数据的修改,一个只读的数据不会造成任何问题。
不变式与竞争条件
这里要提到一个概念:不变式(invariant),意思是关于特定数据结构的陈述始终正确。例如:“x变量保存着列表中元素的个数”,但这些不变式常常在数据更新的时候被破坏。
考虑一个双向链表,节点保存着指向上一个和下一个节点的指针,这里的不变式之一就是”如果你沿着A节点的下一个指针找到了B节点,那么通过B节点的上一个指针也能找到A“,当我们执行删除节点操作时,经历以下步骤:
- 确定删除的节点
- 更新删除节点的上一节点的下一个指针
- 更新删除节点的下一个节点的上一个指针
- 删除节点
在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();//随意访问
}
总之,不要将受保护数据的指针和引用传递到锁保护范围之外的,无论是通过从函数返回值、还是将其存储在外部可见的内存中,还是将其作为参数传递给用户提供的可调用对象中。