异步回调 协程
day36
异步回调与协程
一、异步回调
1、什么是回调:
异步回调指的是:在发起一个异步任务的同时指定一个函数,在异步任务完成时会自动的调用这个函数。
2、为什么需要回调函数
需要获取异步任务的执行结果,但是又不应该让其阻塞(降低效率),即想要高效的获取任务的执行结果。
之前在使用线程池或进程池提交任务时,如果想要处理任务的执行结果则必须调用result函数或是shutdown函数,而它们都是是阻塞的,会等到任务执行完毕后才能继续执行,这样一来在这个等待过程中就无法执行其他任务,降低了效率,所以需要一种方案,即保证解析结果的线程不用等待,又能保证数据能够及时被解析,该方案就是异步回调。
3、如何使用异步回调
通常情况下,异步都会和回调函数一起使用,使用方法即是add_done_callback(),给Future对象绑定一个回调函数。
注意:在多进程中回调函数 是交给主进程来执行 而在多线程中 回调函数是谁有空谁执行(不是主线程)
import requests,re,os,random,time from concurrent.futures import ProcessPoolExecutor def get_data(url): print("%s 正在请求%s" % (os.getpid(),url)) time.sleep(random.randint(1,2)) response = requests.get(url) print(os.getpid(),"请求成功 数据长度",len(response.content)) #parser(response) # 3.直接调用解析方法 哪个进程请求完成就那个进程解析数据 强行使两个操作耦合到一起了 return response def parser(obj): data = obj.result() htm = data.content.decode("utf-8") ls = re.findall("href=.*?com",htm) print(os.getpid(),"解析成功",len(ls),"个链接") if __name__ == '__main__': pool = ProcessPoolExecutor(3) urls = ["https://www.baidu.com", "https://www.sina.com", "https://www.python.org", "https://www.tmall.com", "https://www.mysql.com", "https://www.apple.com.cn"] # objs = [] for url in urls: # res = pool.submit(get_data,url).result() # 1.同步的方式获取结果 将导致所有请求任务不能并发 # parser(res) obj = pool.submit(get_data,url) # obj.add_done_callback(parser) # 4.使用异步回调,保证了数据可以被及时处理,并且请求和解析解开了耦合 # objs.append(obj) # pool.shutdown() # 2.等待所有任务执行结束在统一的解析 # for obj in objs: # res = obj.result() # parser(res) # 1.请求任务可以并发 但是结果不能被及时解析 必须等所有请求完成才能解析 # 2.解析任务变成了串行,
总结:异步回调使用方法就是在提交任务后得到一个Futures对象,调用对象的add_done_callback来指定一个回调函数。
如果把任务比喻为烧水,没有回调时就只能守着水壶等待水开,有了回调相当于换了一个会响的水壶,烧水期间可用作其他的事情,等待水开了水壶会自动发出声音,这时候再回来处理。水壶自动发出声音就是回调。
注意:
-
使用进程池时,回调函数都是主进程中执行执行;
-
使用线程池时,回调函数的执行线程是不确定的,哪个线程空闲就交给哪个线程;
-
回调函数默认接收一个参数就是这个任务对象自己,再通过对象的result函数来获取任务的处理结果。
二、线程中的队列
引入线程队列 : import queue
线程队列方法 :
q = queue.Queue() #实例化对列,先进先出
q = queue.LifoQueue() #实例化队列,后进先出 ( Last in, first out )
q = queue.PriorityQueue() #实例化队列,优先级队列
优先级队列,put() 方法接收的是一个元组,第一个元素是优先级,第二个元素是数据;
优先级可以是数字或字符,只要能够进行大小比较即可(即优先级必须要是能够比较大小的);
如果优先级是字符串或特殊字符,按照字符串或特殊字符的ASCII码比较,如果ASCII码相同,按照先进先出原则取出。
from queue import Queue,LifoQueue,PriorityQueue # 1. 先进先出队列 # q = Queue(1) # q.put("a") # q.put("b",timeout=1) # # print(q.get()) # print(q.get(timeout=2)) # 2.last in first out 后进先出队列(堆栈) # lq = LifoQueue() # lq.put("a") # lq.put("b") # lq.put("c") # # print(lq.get()) # print(lq.get()) # print(lq.get()) # 3.优先级队列 (取出顺序是 由小到大 优先级可以使数字或字符 只要能够比较大小即可) pq = PriorityQueue() # pq.put((2,"b")) # pq.put((3,"c")) # pq.put((1,"a")) # # print(pq.get()) # print(pq.get()) # print(pq.get()) pq.put((["a"],"bdslkfjdsfjd")) pq.put((["b"],"csdlkjfksdjkfds")) pq.put((["c"],"asd;kjfksdjfkdsf")) print(pq.get()) print(pq.get()) print(pq.get())
三、事件
1、什么是事件
线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。
2、Event简述
Event对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行。
## event的常用方法 event.isSet():返回event的状态值; event.wait():如果 event.isSet()==False将阻塞线程; event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度; event.clear():恢复event的状态值为False。
event代码示例:
import time from threading import Thread from threading import Event # 创建一个事件(使用异步修改后) e = Event() #默认False def start(): print("正在启动服务器......") time.sleep(5) print("服务器启动成功!") e.set() # 就是把事件的值设置为True def connect(): for i in range(3): print("等待服务器启动....") e.wait(1) # 会阻塞 直到对方把事件设置为True if e.isSet(): print("连接成功!") break else: print("连接失败") else: #如果3次都没成功 就打印这个消息 print("服务器没有启动") Thread(target=start).start() Thread(target=connect).start()
四、协程
1、引言
上一节中我们知道GIL锁将导致CPython无法利用多核CPU的优势,只能使用单核并发的执行。很明显效率不高,那有什么办法能够提高效率呢?
效率要高只有一个方法就是让这个当前线程尽可能多的占用CPU时间,如何做到?
任务类型可以分为两种 IO密集型 和 计算密集型
对于计算密集型任务而言 ,无需任何操作就能一直占用CPU直到超时为止,没有任何办法能够提高计算密集任务的效率,除非把GIL锁拿掉,让多核CPU并行执行。
对于IO密集型任务任务,一旦线程遇到了IO操作CPU就会立马切换到其他线程,而至于切换到哪个线程,应用程序是无法控制的,这样就导致了效率降低。
如何能提升效率呢?想一想如果可以监测到线程的IO操作时,应用程序自发的切换到其他的计算任务,是不是就可以留住CPU?的确如此
2、单线程实现并发
单线程实现并发这句话乍一听好像在瞎说
首先需要明确并发的定义
并发:指的是多个任务同时发生,看起来好像是同时都在进行
并行:指的是多个任务真正的同时进行
早期的计算机只有一个CPU,既然CPU可以切换线程来实现并发,那么为何不能再线程中切换任务来并发呢?
上面的引子中提到,如果一个线程能够检测IO操作并且将其设置为非阻塞,并自动切换到其他任务就可以提高CPU的利用率,指的就是在单线程下实现并发。
3、如何能够实现并发呢
并发 = 切换任务+保存状态,只要找到一种方案,能够在两个任务之间切换执行并且保存状态,那就可以实现单线程并发
python中的生成器就具备这样一个特点,每次调用next都会回到生成器函数中执行代码,这意味着任务之间可以切换,并且是基于上一次运行的结果,这意味着生成器会自动保存执行状态!
于是乎我们可以利用生成器来实现并发执行:
def task1(): while True: yield print("task1 run") def task2(): g = task1() while True: next(g) print("task2 run") task2()
并发虽然实现了,单这对效率的影响是好是坏呢?来测试一下
可以看到对于纯计算任务而言,单线程并发反而使执行效率下降了一半左右,所以这样的方案对于纯计算任务而言是没有必要的
我们暂且不考虑这样的并发对程序的好处是什么,在上述代码中,使用yield来切换是的代码结构非常混乱,如果十个任务需要切换呢,不敢想象!因此就有人专门对yield进行了封装,这便有了greenlet模块
4、greenlet模块实现并发
def task1(name): print("%s task1 run1" % name) g2.switch(name) # 切换至任务2 print("task1 run2") g2.switch() # 切换至任务2 def task2(name): print("%s task2 run1" % name) g1.switch() # 切换至任务1 print("task2 run2") g1 = greenlet.greenlet(task1) g2 = greenlet.greenlet(task2) g1.switch("jerry") # 为任务传参数
该模块简化了yield复杂的代码结构,实现了单线程下多任务并发,但是无论直接使用yield还是greenlet都不能检测IO操作,遇到IO时同样进入阻塞状态,所以此时的并发是没有任何意义的。
现在我们需要一种方案 即可检测IO 又能够实现单线程并发,于是gevent闪亮登场
协程:是单线程下的并发,又称微线程,纤程。英文名Coroutine。一句话说明什么是线程:协程是一种用户态的轻量级线程,即协程是由用户程序自己控制调度的。
需要强调的是:
#1. python的线程属于内核级别的,即由操作系统控制调度(如单线程遇到io或执行时间过长就会被迫交出cpu执行权限,切换其他线程运行) #2. 单线程内开启协程,一旦遇到io,就会从应用程序级别(而非操作系统)控制切换,以此来提升效率(!!!非io操作的切换与效率无关)
对比操作系统控制线程的切换,用户在单线程内控制协程的切换
优点如下:
#1. 协程的切换开销更小,属于程序级别的切换,操作系统完全感知不到,因而更加轻量级 #2. 单线程内就可以实现并发的效果,最大限度地利用cpu
缺点如下:
#1. 协程的本质是单线程下,无法利用多核,可以是一个程序开启多个进程,每个进程内开启多个线程,每个线程内开启协程来尽可能提高效率 #2. 协程指的是单个线程,因而一旦协程出现阻塞,将会阻塞整个线程
6、gevent协程的使用
import gevent,sys from gevent import monkey # 导入monkey补丁 monkey.patch_all() # 打补丁 import time print(sys.path) def task1(): print("task1 run") # gevent.sleep(3) time.sleep(3) print("task1 over") def task2(): print("task2 run") # gevent.sleep(1) time.sleep(1) print("task2 over") g1 = gevent.spawn(task1) g2 = gevent.spawn(task2) gevent.joinall([g1,g2])
需要注意:
1.协程执行时要想使任务执行则必须对协程对象调用join函数
2.有多个任务时,随便调用哪一个的join都会并发的执行所有任务,但是需要注意如果一个存在io的任务没有被join该任务将无法正常执行完毕
3.monkey补丁的原理是把原始的阻塞模块替换为修改后的非阻塞模块,即偷梁换柱,来实现IO自定切换,所以打补丁的位置一定要放到导入阻塞模块之前