STL库,智能指针和线程安全
STL中的容器是否是线程安全的? 不是.
原因是, STL 的设计初衷是将性能挖掘到极致, 而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响.
而且对于不同的容器, 加锁方式的不同, 性能可能也不同(例如hash表的锁表和锁桶).
因此 STL 默认不是线程安全. 如果需要在多线程环境下使用, 往往需要调用者自行保证线程安全.
智能指针是否是线程安全的?
对于 unique_ptr, 由于只是在当前代码块范围内生效, 因此不涉及线程安全问题.
对于 shared_ptr, 多个对象需要共用一个引用计数变量, 所以会存在线程安全问题.
但是标准库实现的时候考虑到了这个问题, 基于原子操作(CAS)的方式保证 shared_ptr 能够高效, 原子的操作引用计数.
即share_ptr也是线程安全的!但是注意,share_ptr是线程安全的,但并不意味着它所指向的资源也是线程安全的
其它锁介绍
悲观锁:
在每次取数据时,总是担心数据会被其他线程修改,所以会在取数据前先加锁(读锁,写锁,行锁等),当其他线程想要访问数据时,被阻塞挂起。
乐观锁:
每次取数据时候,总是乐观的认为数据不会被其他线程修改,因此不上锁。但是在更新数据前,会判断其他数据在更新前有没有对数据进行修改。主要采用两种方式:版本号机制和CAS操作。
CAS操作:
当需要更新数据时,判断当前内存值和之前取得的值是否相等。如果相等则用新值更新。若不等则失败,失败则重试,一般是一个自旋的过程,即不断重试
自旋锁
我们之前学的互斥锁,申请不到锁的时候,都会被挂起等待
那挂起等待的操作是由谁来完成的呢?
答案还是我们的OS(操作系统)老大.
阻塞或者唤醒一个线程对OS来说其实也会是一种负担,假如一个线程持有锁的时间很短,即OS刚让没有抢到锁的线程阻塞等待,还没等一会儿,持有锁的线程就说自己完成任务了,OS又要把刚阻塞等待的线程唤醒,这样效率就不高!
那有没有其它锁,一直轮询,看锁是否释放了,而不是阻塞等待?
答案是有的,就是我们接下来要介绍的自旋锁.
自旋锁的本质就是轮询,不达目的不罢休
线程获取不到自旋锁,并不会被阻塞等待,而是一直询问拿到锁的线程,什么时候好?一旦完成任务,释放锁,就立马获取锁,省下了线程状态切换的消耗
那什么时候采用互斥锁,什么时候采用自旋锁呢?
关键就是等待时间的长短,即访问临界区要花费多少时间
假如锁被占用的时间很短,那自旋的效果就很不错
反之,假如一直占用,来完成任务,那自旋的效果就大打折扣,白白浪费处理器资源
读写锁
在编写多线程的时候,有一种情况是十分常见的。那就是,有些公共数据修改的机会比较少。
相比较改写,它们读的机会反而高的多。通常而言,在读的过程中,往往伴随着查找的操作,中间耗时很长。给这种代码段加锁,会极大地降低我们程序的效率。
那么有没有一种方法,可以专门处理这种多读少写的情况呢? 有,那就是我们接下来要介绍的读写锁。
读者写者的"321"原则
3: 读者读者(无关系) 写者写者(互斥) 读者写者(互斥,同步)
2:读/写
1:交易场所
可以看到它和我们生产者消费者模型是非常相似的,唯一有一个重大区别就在于在cp问题中,消费者之间是互斥的,但是读者与读者是互相不影响的
原因在于消费者会拿走数据!但是读者并不会,只是访问相应的数据
理解读者写者问题,可以类比于我们小学时出黑板报
看黑板报的同学就是读者,出黑板报的同学就是写者
读者与读者间看黑板报互不影响
但是写者工作的时候,读者就不要看了,看了也没用,都没出完;同理,读者看的时候,写者就不要继续工作了,别人欣赏着,你突然将黑板报全部擦干净,只会得罪别人.
对应的接口
伪代码讲解原理
本质上是采用类似信号量的方式来实现的
最核心的其实就是保证
有一个读者读,写者不能写;但其它读者还可以进
所以最关键的还是读者这部分信号量的申请
写者饥饿问题
由上面的伪代码我们也可以发现
写者这个执行流是很容易长期持有不到锁的,只要reader_cnt一直不为0,也就是有读者读,写者就写不了
这也符合我们有些公共数据修改的机会比较少的场景(修改频率低)
一般默认读者优先,即有一把锁空闲出来,默认读者线程拿
我们还可以设置为写者优先策略,正在读的读者线程依旧可以继续读完,但是比写者线程晚到的读者线程就要往后排一下队.