线程同步
- 概念
- Event
- 练习
- 总结
- wait的使用
- 定时器 Timer/延迟执行
- 总结
- Lock
- 锁的基本使用
- 练习
- 加锁、解锁
- 加锁、解锁常用语句:
- 锁的应用场景
- 非阻塞锁使用
- 可重入锁RLock
- 可重入锁总结
- Condition
- Condition基本使用
- Condition总结
- semaphore 信号量
- release方法超界问题
- BoundedSemaphore类
- 应用举例
- 问题
- 1、边界问题分析
- 2、正常使用分析
- 信号量和锁
Event事件,是线程间通信机制中最简单的实现,使用一个内部的标记flag,通过flag的True或False的变化来进行操作
练习
老板雇佣了一个工人,让他生产杯子,老板一直等着这个工人,直到生产了10个杯子
总结
- 使用同一个Event对象的标记flag
- 谁wait就是等到flag变为True,或等到超时返回False。不限制等待的个数
wait的使用
定时器 Timer/延迟执行
继承自,这个类用来定义延迟多久后执行一个函数
start方法执行之后,Timer对象会处于等待状态,等待了interval秒之后,开始执行function函数的
- 上例代码工作线程早就启动了,只不过是在工作线程中延时了4秒才执行了worker函数
Timer是线程Thread的子类,Timer实例内部提供了一个finished属性,该属性是Event对象。cancel方法,本质上是在worker函数执行前对finished属性set方法操作,从而跳过了worker函数执行,达到了取消的效果
总结
- Timer是线程Thread的子类,就是线程类,具有线程的能力和特征
- 它的实例是能够延时执行目标函数的线程,在真正执行目标函数之前,都可以cancel它
- cancel方法本质使用Event类实现。这并不是说,线程提供了取消的方法
- 锁,一旦线程获得锁,其它试图获取锁的线程将被阻塞
- 锁:凡是存在共享资源争抢的地方都可以使用锁,从而保证只有一个使用者可以完全使用这个资源
锁的基本使用
- 第三个print永久阻塞
- 不阻塞,获取不到返回Fasle
非阻塞时不要设置timeout值,否则会抛ValueError错误
- 执行结果
上例可以看出不管在哪一个线程中,只要对一个已经上锁的锁阻塞请求,该线程就会阻塞。
练习
订单要求生产1000个杯子,组织10个工人生产。请忽略老板,关注工人生成杯子
- 上例中共有三处可以释放锁。只有第二出释放锁的位置正确
- 假设位置1的合适,分析如下:
有一个时刻,在某一个线程中正好是999,,释放锁,正好线程被打断。另一个线程判断发现也是999,,可能线程被打断。可能另外一个线程也判断是999,flag也设置为True。这三个线程只要继续执行到,一定会导致cups的长度超过1000的。 - 假设位置2的合适,分析如下:
在某一个时刻,正好是999,,其它线程试图访问这段代码的线程都阻塞获取不到锁,直到当前线程安全的增加了一个数据,然后释放锁。其它线程有一个抢到锁,但发现已经1000了,只好break打印退出。再其它线程都一样,发现已经1000了,都退出了。
所以位置2 释放锁 是正确的。
但是我们发现锁保证了数据完整性,但是性能下降很多。 - 上例中位置3,是为了保证位置2的方法被执行,否则,就出现了死锁,得到锁的永远没有释放锁
加锁、解锁
- 一般来说,加锁就需要解锁,但是加锁后解锁前,还要有一些代码执行,就有可能抛异常,一旦出现异常,锁是无法释放,但是当前线程可能因为这个异常被终止了,这也产生了死锁。
加锁、解锁常用语句:
- 使用语句保证锁的释放
- 上下文管理,锁对象支持上下文管理
- 计数器类,可以加,可以减
锁的应用场景
- 少用锁,必要时用锁。使用了锁,多线程访问被锁的资源时,就成了串行,要么排队执行,要么争抢执行
- 举例,高速公路上车并行跑,可是到了省界只开放了一个收费口,过了这个口,车辆依然可以在多车道上一起跑。过收费口的时候,如果排队一辆辆过,加不加锁一样效率相当,但是一旦出现争抢,就必须加锁一辆辆过。注意,不管加不加锁,只要是一辆辆过,效率就下降了
- 加锁时间越短越好,不需要就立即释放锁
- 一定要避免死锁
非阻塞锁使用
- 可重入锁,是线程相关的锁。
- 线程A获得可重复锁,并可以多次成功获取,不会阻塞。最后要在线程A中做和acquire次数相同的release
- 执行结果
可重入锁总结
- 与线程相关,可在一个线程中获取锁,并可继续在同一线程中不阻塞多次获取锁
- 当锁未释放完,其它线程获取锁就会阻塞,直到当前持有锁的线程释放完锁
- 锁都应该使用完后释放。可重入锁也是锁,应该acquire多少次,就release多少次
- 构造方法 ,可以传入一个Lock或RLock对象,默认是RLock
Condition基本使用
Condition用于生产者、消费者模型,为了解决生产者消费者速度匹配问题
- 下例只是为了演示,不考虑线程安全问题
- 执行结果
这个例子,可以看到实现了消息的 一对多 ,这其实就是 广播模式
注:上例中,程序本身不是线程安全的,程序逻辑有很多瑕疵,但是可以很好的帮助理解Condition的使用和生产者消费者模型
Condition总结
- 使用方式
使用Condition,必须先acquire,用完了要release,因为内部使用了锁,默认使用RLock锁,最好的方式是使用with上下文
消费者wait,等待通知
生产者生产好消息,对消费者发通知,可以使用notify或者notify_all方法
- 和Lock很像,信号量对象内部维护一个倒计数器,每一次acquire都会减1,当acquire方法发现计数为0就阻塞请求的线程,直到其它线程对信号量release后,计数大于0,恢复阻塞的线程
- 计数器永远不会低于0,因为acquire的时候,发现是0,都会被阻塞
release方法超界问题
- 假设如果还没有acquire信号量,就release,会怎么样?
从上例输出结果可以看出,竟然内置计数器达到了4,这样实际上超出我们的最大值,需要解决这个问题
有界的信号量,不允许使用release超出初始值的范围,否则,抛出ValueError异常
- 连接池
因为资源有限,且开启一个连接成本高,所以,使用连接池 - 一个简单的连接池
连接池应该有容量(总数),有一个工厂方法可以获取连接,能够把不用的连接返回,供其他调用者使用
真正的连接池的实现比上面的例子要复杂的多,这里只是简单的一个功能的实现
- 本例中,get_conn()方法在多线程的时候有线程安全问题
假设池中正好有一个连接,有可能多个线程判断池的长度是大于0的,当一个线程拿走了连接对象,其他线程再来pop就会抛异常的。如何解决?
1、加锁,在读写的地方加锁
2、使用信号量Semaphore
使用信号量对上例进行修改
- 注意:这个连接池的例子不能用到生成环境,只是为了说明信号量使用的例子,连接池还有很多未完成功能
self.conns.append(conn) 这一句有哪些问题考虑?
1、边界问题分析
- 假设一种极端情况,计数器还差1就归还满了,有三个线程A、B、C都执行了第一句,都没有来得及release,这时候轮到线程A release,正常的release,然后轮到线程C先release,一定出问题,超界了,直接抛异常
- 因此信号量,可以保证,一定不能多归还
- 如果归还了同一个连接多次怎么办,重复很容易判断
这个程序还不能判断这些连接是不是原来自己创建的,这不是生成环境用的代码,只是简单演示
2、正常使用分析
- 正常使用信号量,都会先获取信号量,然后用完归还
- 创建很多线程,都去获取信号量,没有获得信号量的线程都阻塞。能归还的线程都是前面获取到信号量的线程,其他没有获得线程都阻塞着。非阻塞的线程append后才release,这时候等待的线程被唤醒,才能pop,也就是没有获取信号量就不能pop,这是安全的
- 经过上面的分析,信号量比计算列表长度好,线程安全
信号量和锁
- 信号量,可以多个线程访问共享资源,但这个共享资源数量有限
- 锁,可以看做特殊的信号量,即信号量计数器初值为1。只允许同一个时间一个线程独占资源
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/rfx/44726.html