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