python协程 --- asyncio库
从线程理解协程
一个cpu想要同时实现多任务的执行,需要操作系统调度cpu去执行多个线程,每个线程执行不同的单个任务,从而实现多任务的执行。线程是操作系统的资源之一,创建或者销毁线程都由操作系统执行,每个线程都由自己独立的资源,例如临时变量的数据,函数调用的堆栈信息,或者当前线程执行的当前位置,发生线程切换时候,线程会保存这些资源,记录当前执行的状态,当cpu在次对该线程发起调度时,恢复上一次的离开时的状态执行。这些数据以及状态信息,通常称为线程上下文。
每个线程都有自己的线程上下文,而线程之间的切换必将引起上下文的切换,cpu执行一个线程时,这部分数据可以直接从寄存器或者多级缓存中读取,读取速度是可观的。线程的切换意味着另一个线程的被唤醒,新线程的数据需要被加载,就可能会覆盖掉部分的上个线程在寄存器或者缓存的数据,下次原线程恢复时需要重新从内存中读取数据到寄存器。多个cpu的情况下,这种方式情况更为严重,因为一个线程可以被随机的被某个cpu执行,上下文切换更为频繁,通常我们会考虑将一个线程绑定到某个cpu上,使其只被这个cpu调度,从而提高效率。
协程
线程的调度更加的耗费系统资源。而使用协程就可以避免这些问题。单个线程的执行是顺序执行的,如果需要完成3个任务(每个任务以一个函数表示),这三个任务将会一个个按照顺序执行,如果每个任务中包含部分IO操作,线程也只能同步阻塞等待IO返回,无法执行其他的任务。而协程实现了在这一个线程中同时调度执行这多个任务,避免这些耗时IO操作的同步阻塞等待,提高了任务的执行效率。
asyncio基本使用
官方文档:https://docs.python.org/zh-cn/3/library/asyncio-task.html#running-an-asyncio-program
定义任务
定义任务使用了async
关键字,然后和普通函数的定义相同
import asyncio async task1(): print("task1 ++++") return "abc" if __name__ = "__main__": asyncio.run(task1()) # run方法在3.7版本才实现
定义了任务task1,然后使用 asyncio.run(task1())
执行了该任务。
在run函数执行任务时,实际上及创建了一个事件循环,这个事件循环会反复监听这个任务的状态,在多个任务时候,还会自动实现多个任务的调度。
事件循环
上面使用run函数运行时,会创建一个事件循环,事件循环中可以添加多个任务,并负责调度这些任务,在3.7之前,就需要手动的创建事件循环,添加任务才能保证事件的运行。
import asyncio async task1(): print("task1 ++++") return "abc" if __name__ == "__main__": loop = asyncio.get_event_loop() # 获得一个事件循环 loop.run_until_complete(task1()) # 运行事件循环,直到task1这个任务结束,事件循环终止
这里只添加了一个task1任务,如果还存在其他的如task2任务需要一并加入,需要使用gather方法
async task1():pass async task2():pass async task3():pass if __name__ == "__main__": loop = asyncio.get_event_loop() # 获得一个事件循环 tasks = asyncio.gather(task1(), task2(), task3()) loop.run_until_complete(tasks)
运行任务
任务的运行需要交给事件循环,事件循环可以同时调度多个任务,前提是这些任务存在IO操作。因为协程任务始终是在一个线程上进行调度的,如果一个任务没有IO(例如无限的死循环),始终占据这个线程,其他的任务将无法被调度执行。
任务可以由事件循环统一调度,这些任务随机的被执行(聚集模式)也可以在一个任务中去启动一个子任务,然后等待子任务执行完毕在继续执行原任务(串行执行模式),等待任务的运行需要使用awati关键字,实现该任务同步阻塞等待子任务的效果。
import asyncio async task1(): print("task1 ++++") return "abc" async task2(): print("task2 ++++") await task1() return 123 if __name__ == "__main__": loop = asyncio.get_event_loop() # 获得一个事件循环 loop.run_until_complete(task2()) # 运行事件循环,直到task1这个任务结束,事件循环终止
loop事件循环启动task2,task2中调用并同步等待task完成。也可以在task2中将task1添加到事件循环中,两个任务协同调用执行。
import asyncio async task1(): print("task1 ++++") return "abc" async task2(): t = asyncio.create_task(task1()) # 在loop中添加一个任务,返回任务对象,该任务将会自动被调用执行 # await t # 也可以等待该t 任务执行完毕。 return 123 if __name__ == "__main__": loop = asyncio.get_event_loop() # 获得一个事件循环 loop.run_until_complete(task2()) # 运行事件循环,直到task1这个任务结束,事件循环终
asyncio.sleep()
该方法也是一个协程对象,该协程对象的功能是等待一定的时常,通常我们在自己的任务中以await的方式调用 asyncio.sleep()方法,使得我们的协程任务等待一定时长再执行,等待时线程可以去执行其他的任务。
等待对象
三种等待对象:
协程对象:async def 方式定义的协程对象
task对象:使用asyncio.create_task方式返回的对象
future对象:
执行回调
task任务可以被注册一个回调函数,调用时会自动注入该task对象作为参数,所以回调函数只能是一参函数。如果回调函数需要有其他参数,需要提前绑定一部分参数值,使成为一参函数作为回调函数(可以使用装饰器,或者functool中的partail方法绑定实现参数绑定)。
import asyncio import functools def callback(task, args): print(task, args) task.result return 123 async task1(): print("task1 ++++") return "abc" async task2(): t = asyncio.create_task(task1()) # 在loop中添加一个任务,返回任务对象,该任务将会自动被调用执行 t.add_callback_done( functools.partail(callback, args="abc") # callback函数绑定了一个args="abc" ) asyncio.sleep(1) return 123