STL线程安全问题,关于STL“只读”函数的加锁
《Windows核心编程》说过,指针、整数这些InterLocked函数族支持的原子操作类型,写操作用InterLocked族函数加锁后,读操作无需加锁,操作系统能够保证读出来的肯定是“修改前”或“修改后”的值,绝不可能读出来修改到一半的值。
现在的问题是,STL里面有一些“只读函数”,比如判定容器是否为空的empty,取队列或链表头结点的front等,都是直接返回的整数和引用(本质是指针),看了下源码,函数过程中也没有任何修改操作,是否参照Windows的InterLocked整数那样可以不加锁?
现在做的项目,为了保险起见兼顾效率,我用Slim RWLock,STL的“写函数”我加了独占锁,“读函数”我加了共享锁,意思就是说同时进入共享锁(同时读)是可以不阻塞的;而一读一写(共享+独占),同时写(独占+独占)都是阻塞的。这个是比较安全的处理方式,效率也兼顾到了。
但是我一直很在意的是,现在是正式的产品化项目我比较谨慎,当初我做demo的时候,还是菜鸟,读操作统统没加锁,但是demo系统运行是很正常的(当时是VS2008SP1的STL,现在是VS2010SP1)。
对于高版本VC的C++标准库的线程安全,我能够确定的是new、delete线程安全,STL的内存分配器也是线程安全,同时读线程安全,写操作肯定线程不安全,问题就在这个“一读一写并且读函数内部全都是用整数和引用处理”的情况。
大家说说看法。
不考虑跨平台,Windows下能保证就行。
[解决办法]
为了严格保证线程安全,读也应该加读锁,即读取的时候也是“独占”的(部分内容独占),否则可能会产生“脏读”。不过这样效率就比较低了。
可以参考数据库的4种隔离级别的实现:
Read Uncommitted
Read Committed
Repeatable Read
Serializable
其中Serializable是最严格的隔离了。
[解决办法]
可能“读脏”,所以只要会被修改的数据,都应该保护起来。
我觉得线程安全是坚决不能忽视的,也是不可完全避免的,所谓的双缓冲队列也只是减少读与写两者的竞争,并不能减少读与读,写与写的竞争。
就我最近的经验来谈,保证并发的情况下提高系统各个功能的响应及时,最好的办法就是尽量适当优化锁粒度,当锁粒度适当的小,就有可能通过逻辑设计的优化留出更多的时间窗口给其他加锁者竞争占用。
千万避免加锁长时间处理数据的情况发生,这将会严重阻碍其他线程的读写请求实时性。
比如你要给vector插入1000条数据,那么最好是这样:
for(...){lock();insert();unlock();}
而不是
lock()
for(...){insert();}
unlock();
应该多从这种方面入手优化,而不是避免锁。
另外,可以通过增加辅助线程处理数据,通过锁粒度优化实现一些高性能的并发。
[解决办法]
读写锁的确是不错的锁,内部实现策略也很巧妙,有写请求便不再多给读机会,这是很必要的。。
但读写锁的建议是读>写,所以还是要适当的情景用适当的东西。
有些设计宁愿多牺牲一些内存,把全局共享数据按需求拷贝给多个线程内部,从而转变访问全局数据为访问局部数据,从而避免加锁,办法还是有的。
但冒险不加锁肯定不行啊,绝对是运气活。
[解决办法]
脏读是可能的!尤其是empty,size这样的依赖具体实现的时候。
http://topic.csdn.net/u/20101217/14/05cb2e93-03e9-4f77-be65-9241393c1df3.html
http://www.chinaunix.net/jh/23/804826.html
帖子比较长,看的时候要耐心些。
[解决办法]
一读一写并且读函数内部全都是用整数,也不保证多核下安全啊。
就算加了volatile,在标准c、c++下也还是不安全的啊。
要c++11或者vc2005以上扩展volatile的语义后才勉强啊。
而如果多个整数同时一读一写,好像扩展volatile都不够了吧。
貌似比较新的一本windows多任务编程的书,暗红皮的,比较权威吧,微软的人写的,貌似主讲语言是C#的。
[解决办法]
现在不是流行lock-free么, 要不折腾下都搞成无锁的.
x64下可以cas16个字节了, 应该足够搞相当复杂的无锁结构了. 那些变态们不是在只有cas8字节也搞了个无锁的内存分配么...
你那个用法肯定有问题, lock前缀会保证高速缓存的一致性. stl 可没这个保证.
[解决办法]
最近我接手的项目中也有这方面的需要,查了半天找到了以下的内容:
http://www.cnblogs.com/super119/archive/2011/04/10/2011415.html
STL container classes thread safe(Microsoft C++ implementation)?
Answer from MSDN:
The container classes are vector, deque, list, queue, stack, priority_queue, valarray, map, hash_map, multimap, hash_multimap, set, hash_set, multiset, hash_multiset, basic_string, and bitset.
A single object is thread safe for reading from multiple threads. For example, given an object A, it is safe to read A from thread 1 and from thread 2 simultaneously.
If a single object is being written to by one thread, then all reads and writes to that object on the same or other threads must be protected. For example, given an object A, if thread 1 is writing to A, then thread 2 must be prevented from reading from or writing to A.
It is safe to read and write to one instance of a type even if another thread is reading or writing to a different instance of the same type. For example, given objects A and B of the same type, it is safe if A is being written in thread 1 and B is being read in thread 2.
[解决办法]
[解决办法]
现在的问题是,STL里面有一些“只读函数”,比如判定容器是否为空的empty,取队列或链表头结点的front等,都是直接返回的整数和引用(本质是指针),看了下源码,函数过程中也没有任何修改操作,是否参照Windows的InterLocked整数那样可以不加锁?
---
我觉得有些情况,对一个指针或者一个整数的读访问,加锁包括锁总线是没有意义的(强调是某些情况)。它虽然不会引发线程安全的问题,但是...打个比方,即使empty()是线程安全的
- C/C++ code
if(container.empty()){ //代码跑进条件判断就能认为container是空的吗?即使在个块里面不操作容器。}
[解决办法]
楼主提及的《windows核心编程》我估计是第五版的,里面介绍了vista以后系统的一些多线程函数。以前的版本是没有这么函数的。不过楼主可以看一下第四版,里面的第十章《线程同步工具包》专门介绍jeffrey自己封装的“单写多读锁”,兼容windows的各种平台。而且这一章里有不少很实用的多线程工具类的封装,我想jeffrey封装的,质量应该可以保证吧?
我接触的多线程程序,一般没有高频率的跨线程读写操作,所以没有太高的效率要求。一般我会用boost的mutex封装一下stl的list或map之类的容器,保证操作的原子性。大概如下面的样子:
- C/C++ code
template<typename T>class safe_list: public noncopyable{public: typedef typename::list<T>::value_type value_type; typedef typename::list<T>::allocator_type::size_type size_type; typedef typename::list<T>::reference reference; typedef typename::list<T>::const_reference const_reference; typedef typename::list<T>::iterator iterator; typedef typename::list<T>::const_iterator const_iterator; public: template<class InputIterator> void assign(InputIterator first, InputIterator last) { mutex::scoped_lock lock(m_mutex); m_list.assign(first, last); } const_iterator begin() const { mutex::scoped_lock lock(m_mutex); return m_list.begin(); } iterator begin() { mutex::scoped_lock lock(m_mutex); return m_list.begin(); } const_iterator end() const { mutex::scoped_lock lock(m_mutex); return m_list.end(); } iterator end() { mutex::scoped_lock lock(m_mutex); return m_list.end(); } iterator erase(iterator position) { mutex::scoped_lock lock(m_mutex); return m_list.erase(position); } iterator erase(iterator first, iterator last) { mutex::scoped_lock lock(m_mutex); return m_list.erase(first, last); } void clear() { mutex::scoped_lock lock(m_mutex); m_list.clear(); } bool empty() const { mutex::scoped_lock lock(m_mutex); return m_list.empty(); } size_type size() const { mutex::scoped_lock lock(m_mutex); return m_list.size(); } reference front() { mutex::scoped_lock lock(m_mutex); return m_list.front(); } const_reference front() const { mutex::scoped_lock lock(m_mutex); return m_list.front(); } void push_front(const T& val) { mutex::scoped_lock lock(m_mutex); m_list.push_front(val); } void push_back(const T& val) { mutex::scoped_lock lock(m_mutex); m_list.push_back(val); } void pop_front() { mutex::scoped_lock lock(m_mutex); m_list.pop_front(); }private: list<T> m_list; mutable mutex m_mutex;};