目录
线程安全和重入
死锁
STL中的容器不是线程安全的
线程安全的单例模式
自旋锁
读者写者问题
线程安全和重入
线程安全:多个线程并发执行同一段代码时,不会出现不同的(异常的)结果,我们就说线程是安全的。常见于对全局变量或静态变量进行操作,并且没有锁保护的情况下,会出现该问题
重入:同一个函数被不同的执行流调用,当前一个执行流还没有执行完,就有其他的执行流再次进入,我们就称之为重入。如果一个函数在重入的情况下,运行结果不会出现任何不同或任何问题,则该函数被称为可重入函数,否则叫不可重入函数
线程安全说的是线程在执行中的相互关系,而重入说的是函数的特点,它们其实区别还是很大的,它们之间的关系我们可以理解为下图:
常见的线程不安全的情况:
1.不保护共享变量的函数
2.函数状态随着被调用,状态发生变化的函数
3.返回指向静态变量指针的函数
4.调用线程不安全的函数
常见的线程安全的情况:
1.每个线程对全局变量或静态变量只有读取的权限,而没有写入的权限,一般来说这些线程是安全的
2.类或者接口对于线程来说都是原子操作
3.多个线程之间的切换不会导致该接口的执行结果存在二义性
死锁
死锁顾名思义就是锁死了,就是说代码不会向下正常推进了,举一个最简单的例子:我们错把unlock仍写成了lock,此时线程就会因为申请不到锁而一直阻塞在这里,当然这只是为了理解什么叫死锁
举一个更加普遍的例子,线程1需要先申请锁1再申请锁2才可以访问资源,线程2需要先申请锁2再申请锁1才可以访问资源,当线程1申请完锁1,线程2申请完锁2后由于它们再也申请不到另一把锁了,所以就会一直阻塞,就会出现死锁问题
由上面的例子我们可以得到产生死锁的四个必要条件
1.互斥条件:一个资源每次只能被一个执行流使用。就是说要加锁
2.请求与保持条件:我既保持我的资源(锁),我还要你的资源
3.不剥夺条件:一个执行流已获得的资源,在未使用完之前,不能强行剥夺。就是说不会去抢别人的锁
4.循环等待条件:线程之间形成一种头尾相连的循环等待资源的关系
上面说的是必要条件,就是如果产生死锁,那么四个条件必然发生,那么我们只要破坏掉其中的一个条件即可
1.破坏第一个条件就是非必要不加锁
2.破坏第二个条件就是不保持条件了,如果拿着锁1,申请不到锁2,那么干脆把锁1也释放掉
3.剥夺别人的锁,释放掉别人的锁
4.尽量不要构成循环等待,每个线程申请锁的顺序要一致
STL中的容器不是线程安全的
因为STL的设计初衷就是为了将性能挖掘到极致,而一旦涉及到加锁保证线程安全,会对性能造成巨大的影响
智能指针是否线程安全?
对于unique_ptr由于只是在当前代码块范围内生效,因此不涉及线程安全问题
对于shared_ptr,多个对象共用一个引用计数变量,所以会存在线程安全问题,标准库实现的时候考虑到了这个问题,基于原子操作保证它是线程安全的
线程安全的单例模式
单例模式就是只创建一个对象,通常有饿汉和懒汉两种方式,饿汉是在主函数加载之前就创建出对象,懒汉是第一次用的时候才创建对象
饿汉是线程安全的,因为程序加载后多个线程只是去获取对象的指针,而懒汉我们则需要通过加锁的方式实现线程安全,那么我们下面可以改写线程池使线程池支持单例模式
我们就是创建静态的对象指针,创建一个静态的获取对象的函数,构造私有化,禁用拷贝构造和赋值重载
自旋锁
自旋其实就是一个不断询问的过程,我们是否用自旋锁就跟线程在临界区中执行时间的长短有关了,如果执行时间长的话,比如有IO操作或者网络请求,我们就用之前学的普通的锁,线程会挂起等待;如果执行时间短,比如都是内存级的操作,我们就可以不必挂起等待,而是不断的询问申请锁
读者写者问题
读者之间没有关系,因为读者跟之前的消费者不同,读者并不会把数据拿走;读者和写者之间是互斥和同步的关系,写到一半不能读,会导致数据不一致的问题;写者之间是互斥关系
他们之间的关系可以用下面的伪代码来表示
我们上面的接口默认就是读者优先的,会有写者饥饿问题
我们如果想让写者优先,可以让没来读的人先别来读了,让正在读的人读完