目录
一、信号量
编辑
1.1、信号量的原理
二、信号量的接口
2.1、初始化接口:
2.2、等待接口
2.3、释放接口:
2.4、销毁接口:
三、生产者与消费者信号量代码实现:
四、线程池
3.1、应用场景
3.2、线程池的原理:
3.3、构造线程池要做的事情
3.4、代码实现
3.5、线程池的线程该如何退出
五、读写锁
5.1、应用场景:
5.2、读写锁的三种状态
4.3、读写锁的接口:
代码验证下读写锁
读写锁问题
六、单例模式
单例模式的两种形式
7、乐观锁&悲观锁
自旋锁 (busy-waiting类型) 和互斥锁 (sleep-waiting类型)的区别:
信号量是什么?
信号量既能完成互斥,也能够完成同步
int sem_post(sem_t *sem);
1.会对资源计数器进行加一操作。
2.判断资源计数器的值是否小于等于0
测试同步
我们先来写个demo代码测试下这几个接口的功能
我们运行一下来看看
我们可以看到,确实是什么也没有输出,我们为了确保,再来看看调用堆栈
我们能够看到,确实是阻塞在这里了
我们再来修改下代码,来验证下sem_post函数的作用
我们来运行一下
发现发送了2号信号之后,wait取消了
测试互斥
我们来运行一下试试
发现确实能够达到互斥锁的功能
线程安全的队列(用信号量来进行维持)
生产者消费者线程的创建
我们来运行一下,来观察结果会是怎么样的
我们能够发现,利用信号量也能成功实现生产者和消费者的模型
线程池不仅要提高程序运行效率,还要提高程序处理业务的种类
1、提高程序运行效率自然就是创建多个线程
2、提高处理业务的种类:
我们使用if else switch 能够实现
但是这些方法存在缺陷,当业务种类比较多的时候,分支语句就不适用了(我们总不可能写100多个分支语句吧)
线程池=一堆线程+线程安全的队列(元素带有任务接口)
代码
我们刚刚的代码是因为主线程退出了,导致工作线程都进行退出了
我们要考虑 : 怎么让整个进程是一种优雅退出的方式:
优雅: 1、线程是自己退出的(收到了某一种指令),而不是因为进程退出了,而被迫销毁
2、队列的元素没有待要处理的
我们运行一下
现在如果有线程1和线程2在对一个资源进行读模式获取,此时来了一个线程3要以写的方式获取这把读写锁,然后又来了一个线程D要进行读取,这里问线程C要不要等待线程D也读取完之后再对资源进行写
答案:
读3不能插写1的队,因为读写锁有个机制:
如果读写锁已经在读模式打开了,有一个线程A想要以写模式打开获取读写锁,则需要等待,如果在等待期间,又来了读模式加锁的线程,那读模式的线程要等待写线程先执行完再说,因为如果这时读取的线程可以获取这把锁,读写锁本来就是大量读少量写的使用场景,那么就会导致写的线程一直拿不到这把锁,这是不合理的。线程就会饥饿。
单例模式是什么?
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点主要解决:一个全局使用的类频繁地创建与销毁。
当您想控制实例数日,节省系统资源的时候。何时使用:
判断系统是否已经有这个单例,如果有则返回,如果没有则创建如何解决:
关键代码 : 构造函数是私有的。
1.饿汉模式: 在程序启动的时候就创建唯一的实例对象,饿汉模式不需要加锁
代码:
2.懒汉模式: 当你第一次使用的时候才创建一个唯一的实例对象,从而实现延迟加载效果
懒汉模式在第一次使用单例对象时才完成初始化工作,因此可能存在多线程竞态环境,如果不加锁会导致重复工造或者构造不安全问题,所以需要加锁
代码:
我们重点来分析下这里面的GetInstance()函数
当代码是这样的时候,对于单线程代码来说,这已经是正确的了,但是如果是多线程呢?
如果是多个线程的话,就有可能发生错误,创建多个不同的类对象,为了解决这个问题,我们可以给临界代码加锁
这样确实是能够保证只创建出来一个实例化对象了,但是仔细想来这样效率很低,
线程A已经创建了一个对象,那么线程B,线程C,线程D、、、在获取这个对象的时候,就需要先等待锁,那就太复杂了,因为我们要知道,先前加锁是为了只创建一个对象,但现在已经有唯一的对象了,再去加锁就太复杂了,所以我们要再加一层if判断语句
这个时候,假如再有线程过来,这里的 st ! = NULL,就会直接返回 st 的值,效率就会很高
悲观锁:
针对某个线程访问临界区修改数据的时候,都会认为可能有其他线程并行修改的情况发生, 所以在线程修改数据之前就进行加锁,让多个线程互斥访问。 悲观锁有: 互斥锁,读写锁,自旋锁等等
乐观锁:
针对某个线程访问临界区修改数据的时候,乐观的认为只有该线程在修改, 大概率不会存在并行的情况。所以修改数据不加锁,
但是, 在修改完毕, 进行更新的时候,进行判断 例如:(版本号控制, CAS无锁编程)
1.自旋锁加锁时,加不到锁,线程不会切换 (时间片没有到的情况, 时间片到了, 也会线程切换),会持续的尝试拿锁, 直到拿到自旋锁
2.互斥锁加锁时, 加不到锁,线程会切换(时间片没有到, 也会切换), 进入睡眠状态, 当其他线程释放互斥锁(解锁)之后, 被唤醒。在切换回来,进行抢锁
3.自旋锁的优点:因为自旋锁不会引起调用者睡眠,所以自旋锁的效率远高于互斥锁。
4.自旋锁的缺点:自旋锁一直占用着CPU,他在未获得锁的情况下,一直运行(自旋),所以占用着CPU,如果不能在很短的时间内获得锁,这无疑会使CPU效率降低
5.适用于临界区代码较短时(直白的说: 临界区代码执行时间短)的情况, 使用自旋锁效率比较高。因为线程不用来回切换
6.当临界区当中执行时间较长, 自旋锁就不适用了, 因为拿不到锁会占用CPU一直抢占锁。
自旋锁的代码: