STL,智能指针和线程安全自选锁读者写者问题
目录
STL中的容器是否是线程安全的?
智能指针是否是线程安全的?
其他常见的各种锁
读者写者问题(了解)
读写锁
读写锁接口
读写锁伪代码
STL中的容器是否是线程安全的?
不是.
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全, 会对性能造成巨大的影响. 而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶). 因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
智能指针是否是线程安全的?
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题. 但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
线程安全share_ptr的实现如下:
template<class T>
class shared_ptr
{
public:// RAIIshared_ptr(T* ptr = nullptr):_ptr(ptr), _pcount(new atomic<int>(1)){}template<class D>shared_ptr(T* ptr, D del): _ptr(ptr), _pcount(new atomic<int>(1)), _del(del){}// function<void(T*)> _del;void release(){if (--(*_pcount) == 0){//cout << "delete->" << _ptr << endl;//delete _ptr;_del(_ptr);delete _pcount;}}~shared_ptr(){release();}shared_ptr(const shared_ptr<T>& sp):_ptr(sp._ptr), _pcount(sp._pcount){++(*_pcount);}// sp1 = sp3shared_ptr<T>& operator=(const shared_ptr<T>& sp){if (_ptr != sp._ptr){release(); //释放sp1原先的地址_ptr = sp._ptr;_pcount = sp._pcount;++(*_pcount);}return *this;}// 像指针一样T& operator*(){return *_ptr;}T* operator->(){return _ptr;}int use_count() const{return *_pcount;}T* get() const{return _ptr;}private:T* _ptr;atomic<int>* _pcount;function<void(T*)> _del = [](T* ptr) {delete ptr; };
};
虽然share_ptr本身是线程安全的,但是它里面的资源并不是线程安全的
void test_shared_ptr()
{size_t n1 = 10000;size_t n2 = 10000;mutex mtx;shared_ptr<double> sp(new double(1.1));atomic<size_t> x = 0;thread t1([&]() {for (size_t i = 0; i < n1; i++){shared_ptr<double> copy1(sp);++(*copy1);shared_ptr<double> copy2(sp);}});thread t2([&]() {for (size_t i = 0; i < n2; i++){shared_ptr<double> copy1(sp);++(*copy1);shared_ptr<double> copy2(sp);}});t1.join();t2.join();cout << sp.use_count() << endl;cout << *sp << endl;
}
运行结果:
所以当我们访问SmartPtr中的资源时,需要进行加锁保护
其他常见的各种锁
悲观锁:在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行 锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁:每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前, 会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。 CAS操作:当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不 等则失败,失败则重试,一般是一个自旋的过程,即不断重试。
自旋锁:申请锁失败之后不会返回,而是一直询问锁资源是否就绪,但是访问临界区时间长的话非常占用CPU资源,自旋锁适用于访问临界区时间短的场景,而访问临界区时间长的场景,外面一般使用挂起等待锁
读者写者问题(了解)
读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地 降低我们程序的效率。那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是读写锁。
注意:写独占,读共享,读锁优先级高
读写锁接口
int pthread_rwlockattr_setkind_np(pthread_rwlockattr_t *attr, int pref);
功能:设置某个读写锁属性
函数参数说明:
attr:读写锁属性
pref:pref 共有 3 种选择
- PTHREAD_RWLOCK_PREFER_READER_NP (默认设置) 读者优先,可能会导致写者饥饿情况
- PTHREAD_RWLOCK_PREFER_WRITER_NP 写者优先,目前有 BUG,导致表现行为和PTHREAD_RWLOCK_PREFER_READER_NP 一致
- PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP 写者优先,但写者不能递归加锁
注意:读写锁的适用场景就是读多写少的情况
初始化
int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,const pthread_rwlockattr_t
*restrict attr);
第一个参数是读写锁变量,第二个参数是读写锁的属性一般不需要管
销毁
int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
加锁和解锁
int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock); //读锁
int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock); //写锁int pthread_rwlock_unlock(pthread_rwlock_t *rwlock); //解锁
读写锁伪代码
与生产者消费者模型一样,遵循321原则
3种关系:写者和写者(互斥), 读者和读者(共享), 读者和写者(互斥,同步,读者优先)
2个角色:写者和读者
1个交易场所:数据交换的地点
读者优先的伪代码
读者伪代码:
int reader_count = 0; //允许多个读者同时进行读操作,适用引用计数的原理进行维护//读者的加锁&&解锁
lock(&rlock);
reader_count++;
if(reader_count == 1) lock(&wlock); //只要有一个读者进行读操作,就让写者停止写入
unlock(&rlock);//读取操作//写者的加锁&&解锁
lock(&rlock);
reader_count--;
if(reader_count == 0) unlock(&wlock); //读者全部退出之后,为写者进行解锁
unlock(&rlock);
写者伪代码:
lock(&wlock);//写入操作unlock(&wlock);