线程队列、事件以及协程
线程的几个队列
都是从queue这个模块中导入
1、Queue队列(先进先出的队列)
from queue import Queue q = Queue(maxsize=3) # 实例化产生队列对象 # maxsize 设置队列里能容纳的最大的数据个数 q.put("first") q.put("second") q.put("third") # 如果队列满了,put会阻塞住,等到空了再放进去 print(q.get()) # first print(q.get()) # second print(q.get()) # third 如果队列空了,get会阻塞住,等到有值再取出来 # 从结果中可以看出,queue.Queue 实例化出来的对象是先进先出
2、LifoQueue队列(先进后出的队列,lifo 是 last in first out缩写)
from queue import LifoQueue q = LifoQueue() q.put("first") q.put("second") q.put("third") print(q.get()) # third print(q.get()) # second print(q.get()) # first # 从结果可以看出,queue.LifoQueue 实例化出来的对象是先进的后出
3、PriorityQueue队列(存储数据时可以设置优先级的队列)
--1、如果只放一个元素,不用考虑元素能不能比较大小
--2、只要队列里元素超过两个,那么元素之间必须可以支持比较大小
from queue import PriorityQueue q = PriorityQueue() q.put(item={"a":3}) # 这里需要注意的是,如果放入队列是多个元素,那么元素之间必须支持比大小 q.put({"A":2}) print(q.get()) # A print(q.get()) # a 根据比较完的大小,小的先出 # 我们其实可以自定义一些类,然后给他添加比大小的方法,就可以比较大小了
事件Event
1、什么是事件
事件表示在某个事件发生了某个事情的通知信号,用于线程间的协同工作
因为不同线程之间是独立运行的状态不可预测,所以一个线程与另一个线程间的数据是不同步的,
当一个线程需要利用另一个线程的状态来确定自己的下一步操作时,就必须保持线程间数据的同步
2、Event介绍
event对象包含一个可由线程设置的信号标志,它允许线程等待某些事情发生后才执行
在初始的情况下,event对象中的信号标志被设置为假,
如果有一个线程等待一个event对象,而这个event对象的标志为假,那么将一直阻塞到标志变为真才接着执行
3、Event使用
可用方法
from threading import Event,Thread e = Event() e.set() # 将event对象的标记设为True,所有被阻塞的线程就进入就绪态,等待操作系统调度 e.is_set() # 返回event对象的标记状态 e.wait(timeout=2) # 如果event对象的标记为False,那就阻塞,True就不阻塞 # 里面可以设置超时时间,如果阻塞超过时间就会接着往下执行 e.clear() # 把event对象的标记重新设为False
使用案例
# 需求是:两个任务并发执行,但是task2里的over必须要等到task1完毕才能执行 import time from threading import Event,Thread e = Event() def task1(): print("task1 run") time.sleep(3) print("task1 over") e.set() # task1 执行完毕,将事件对象设置为True,让task2里的阻塞变为就绪态,等待操作系统调度 def task2(): print("task2 run") time.sleep(1) e.wait() # 让他在这里等待,知道事件对象被设置为True print("task2 over") Thread(target=task1).start() # 这里简写,把创建对象和启动线程合并到一起 Thread(target=task2).start()
协程*****
协程的目的就是要在单线程中实现并发‘
为什么要在单线程内实现并发
1、这样我们可以自己来掌控cpu运行的调度,只要cpu一过来,基本上就能把它的时间片用光,提高程序的效率
2、当我们并发量比较高的时候,并且线程因为硬件原因已经不能再开启了,这是协程就是一个可以不占
多少资源,又能实现并发的方法
单线程并发特点
1、对于计算密集型任务而言,单线程并发并不能提高性能,反而会降低效率
2、对于IO操作而言,必须具备能够检测IO操作,并自动切换到其他任务,这样才能提高效率
实现单线程并发的方式
1、yield保存状态 + 手动切换(了解)
并发:多个任务看起来是同时运行,本质上是切换 + 保存状态
在生成器中,yield就可以保存当前函数的运行状态
所以我们可以简单通过yield来实现一个线程内的并发
yield实现的方法是不能检测IO操作的
def task1(): print("task1 first") yield print("task1 second") yield def task2(): print("task2 first") task1().__next__() # 找到task1中第一个yield会返回来接着往下执行 print("task2 second") task1().__next__() # 需要注意的是,next方法必须要找到生成器中的yield,否则报错 task2() # 最终实现单线程间的并发
2、greenlet封装切换(了解)
直接使用yield虽然能实现并发了,但是代码的结构太乱(到处都是yield和next),
greenlet也是需要手动切换,并且不能检测IO操作
import greenlet def task1(): print("task1 first") g2.switch() print("task2 second") g2.switch() def task2(): print("task2 first") g1.switch() print("task2 second") g1 = greenlet.greenlet(task1) g2 = greenlet.greenlet(task2) g1.switch() # 切换到g1去,执行完毕再接着往下执行,都是需要手动切换 print("main over")
3、gevent协程
--1、什么是协程
gevent 就是协程
协程也是轻量级线程,也可以称之为微现场
它是应用程序级别的任务调度方式
它可以实现任务之间的自动切换,但是无法检测IO,需要和猴子补丁(monkey模块)一起使用
--2、协程对比线程
协程是应用程序级别调度,线程是操作系统级别调度
应用程序级别的调度
我们在检测到IO操作时,可以立马切换到我的其他任务来执行,如果有足够多的任务执行
就可以把cpu的时间片充分利用起来
操作系统级别的调度
遇到IO,操作系统就会拿走cpu,下一次分给哪一个线程就要看操作系统内部算法,不是我们能决定的
所以明显可以看出对于一个进程而言,使用协程的效率要高于使用多线的效率
--3、如何提高效率(使用场景)
在Cpython中,由于GIL锁的存在,导致多线程并不能并行,丧失了多核优势
即使开启了多线程也只能并发,这时完全可以用协程来实现并发,提高程序的效率
优点:不会占用更多无用的资源
缺点:如果是计算密集型任务,使用协程反而降低效率
--4、配合猴子补丁的用法
import time from gevent import monkey # 先导入monkey的类 monkey.patch_all() # 可以修改一些阻塞代码变成非阻塞代码(具体可以修改哪些可以点进去看) import gevent # 导入gevent def task1(): print("task1 first") time.sleep(1) print("task1 second") def task2(): print("task2 first") print("task2 second") g1 = gevent.spawn(task1) # 创建协程 g2 = gevent.spawn(task2) gevent.joinall([g1,g2]) # 注意的是主线程需要等待这些任务完成,不然主线程已结束,任务都不会执行 print("main over") # 运行结果也可以看出这些任务之间都是并发执行的,并且遇到IO可以自动切换