12. STL容器的线程安全性

STL提供的线程安全性

 
基本上STL容器提供的线程安全性只有以下两点:

  1. 多个线程读取是安全的
    多个线程可以读同一个容器内的数据,读时不允许写操作
  2. 多个线程对不同的容器写入是安全的

如何实现线程安全性

 
完全的线程安全很难实现,一个库可能试图以下列方式实现这样完全线程安全的容器:

  1. 对于容器成员函数的每一次调用都锁住该容器直到调用完成。
  2. 在容器返回的迭代器生存期结束之前锁住容器
  3. 对于作用于容器的每个算法,在算法执行期间都锁住容器。(实际上这一点毫无必要,因为算法无法识别它们正在操作的容器)

实例

 
现在考虑一个例子:将vector内的第一个5改成0,如果它存在的话。

1
2
3
4
5
vector<int> v;
auto first5 = find(v.begin(), v.end(), 5);
if (first5 != v.end()){
*first5 = 0;
}

在多线程运行环境中,另一个线程可能在find操作完成后立刻修改v中的数据。那样第三行与v.end的检测将毫无意义,因为v的元素已经不再是原来的元素。并且如果执行了插入或者删除操作,first5也已经失效。
之前列举的3个方法都无法防止上述问题,迭代器调用返回得很快,生存期只有一行,find也是,无法帮助锁定。


解决方案

 
为了保证上述代码的线程安全,v必须从行1到行3都保持锁定,STL无法做到自动判断容器是否需要锁定,因此必须手动完成:

1
2
3
4
5
6
7
8
vector<int> v;
...
getMutexFor(v);//需要自己实现的容器锁操作
auto first5 = find(v.begin(), v.end(), 5);
if (first5 != v.end()) {
*first5 = 0;
}
releaseMutexFor(v);

从面向对象的角度而言,解决方法是构建一个lock类,其类模板大致如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
Template<typename Container>
class Lock {
public:
Lock(const Containers container)
: c(container){
getMutexFor(c);
}
~Lock(){
releaseMutexFor(c);
}
private:
const Container& c;
};

显然,这种方法是在用类来管理资源的生存期(RAII),其使用案例如下:
1
2
3
4
5
6
7
8
9
10
vector<int> v;
...
{
Lock<vector<int> > lock(v);
auto first5 = find(v.begin(), v.end(), 5);
if (first5 != v.end()) {
*first5 = 0;
}
...//lock对象析构,v解锁
}

另外,这种解决方案是异常安全的,因为在异常发生的情况下,局部对象会被销毁,此时lock也会释放互斥量


总结

 
STL允许在一个容器上的多线程读取和不同容器上的多线程写入,除此之外不能依赖任何库自带的线程安全性。