STL学习笔记之容器线程安全

考虑下列代码。它搜寻一个vector<int>中第一次出现5这个值的地方,而且,如果它找到了,就把这个值改为0。

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

在多线程环境里,另一个线程可能在行1完成之后立刻修改v中的数据。如果是那样,行2对first5和v.end的检测将是无意义的,因为v的值可能和它们在行1结束时的值不同。实际上,这样的检测会产生未定义的结果,因为另一线程可能插在行1和行2之间,使first5失效,或许通过进行一次插入操作造成vector重新分配它的内在内存。(那将使vector全部的迭代器失效。关于重新分配行为的细节,参见条款14。)类似的,行3中对*first5的赋值是不安全的,因为另一个线程可能在行2和行3之间执行,并以某种方式使first5失效,可能通过删除它指向(或至少曾经指向)的元素。

在上面列举的锁定方法都不能防止这些问题。行1中begin和end调用都返回得很快,以至于不能提供任何帮助,它们产生的迭代器只持续到这行的结束,而且find也在那行返回。

要让上面的代码成为线程安全的,v必须从行1到行3保持锁定,很难想象STL实现怎么能自动推断出这个。记住同步原语(例如,信号灯,互斥量,等等)通常开销很大,更难想象实现怎么在程序没有明显性能损失的情况下做到前面所说的——以这样的一种方式设计——让最多一个线程在1-3行的过程中能访问v。

这样的考虑解释了为什么你不能期望任何STL实现让你的线程悲痛消失。取而代之的是,你必须手工对付这些情况中的同步控制。 在这个例子里,你可以像这样做:

vector<int> v;
...
getMutexFor(v);
vector<int>::iterator first5(find(v.begin(), v.end(), 5));
if (first5 != v.end()) {						// 这里现在安全了
	*first5 = 0;						// 这里也是
}
releaseMutexFor(v);

一个更面向对象的解决方案是创建一个Lock类,在它的构造函数里获得互斥量并在它的析构函数里释放它,这样使getMutexFor和releaseMutexFor的调用不匹配的机会减到最小。这样的一个类(其实是一个类模板)基本是这样的:

template<typename Container>				// 获取和释放容器的互斥量
class Lock {						// 的类的模板核心;
public:							// 忽略了很多细节
	Lock(const Containers container)
			: c(container)
	{
		getMutexFor(c);				// 在构造函数获取互斥量
	}

	~Lock()
	{
		releaseMutexFor(c);			// 在析构函数里释放它
	}

private:
	const Container& c;
};

使用一个类(像Lock)来管理资源的生存期(例如互斥量)的办法通常称为资源获得即初始化,你应该能在任何全面的C++教材里读到它。一个好的开端是Stroustrup的《The C++ Programming Language》[7],因为Stroustrup普及了这个惯用法,但你也可以转到《More Effective C++》的条款9。不管你参考了什么来源,记住上述Lock是被剥离到最原始的本质的。一个工业强度的版本需要很多改进,但是那样的扩充与STL无关。而且这个最小化的Lock已经足够看出我们可以怎么把它用于我们一直考虑的例子:

vector<int> v;
...
{								// 建立新块;
	Lock<vector<int> > lock(v);					// 获取互斥量
	vector<int>::iterator first5(find(v.begin(), v.end(), 5));
	if (first5 != v.end()) {
		*first5 = 0;
	}
}								// 关闭块,自动
								// 释放互斥量

因为Lock对象在Lock的析构函数里释放容器的的互斥量,所以在互斥量需要释放是就销毁Lock是很重要的。为了让这件事发生,我们建立一个里面定义了Lock的新块,而且当我们不再需要互斥量时就关闭那个块。这听上去像我们只是用关闭新块的需要换取了调用releaseMutexFor的需要,但是这是错误的评价。如果我们忘记为Lock建立一个新块,互斥量一样会释放,但是它可能发生得比它应该的更晚——当控制到达封闭块的末端。如果我们忘记调用releaseMutexFor,我们将不会释放互斥量。

而且,这种基于Lock的方法在有异常的情况下是稳健的。C++保证如果抛出了异常,局部对象就会被销毁,所以即使当我们正在使用Lock对象时有异常抛出,Lock也将释放它的互斥量。如果我们依赖手工调用getMutexFor和releaseMutexFor,那么在调用getMutexFor之后releaseMutexFor之前如果有异常抛出,我们将不会释放互斥量。

异常和资源管理是重要的,但是它们不是本条款的主题。本条款是关于STL里的线程安全。当涉及到线程安全和STL容器时,你可以确定库实现允许在一个容器上的多读取者和不同容器上的多写入者。你不能希望库消除对手工并行控制的需要,而且你完全不能依赖于任何线程支持。

以上内容摘自《Effective STL》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值