协程
在CPython解释器中,由于GIL的原因,导致线程性能严重下降,实际可以认为是伪线程,单线程
于是乎,为了弥补遗失的性能,在单线程模型下,Python又推出了协程
协程:又称微线程,纤程。协程是一种用户态的轻量级线程。
- 线程的切换会保存到CPU的栈里,协程拥有自己的寄存器上下文和栈,
- 协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈
- 协程能保留上一次调用时的状态(即所有局部状态的一个特定组合),每次过程重入时,就相当于进入上一次调用的状态
- 协程最主要的作用是在单线程的条件下实现并发的效果,但实际上还是串行的(像yield一样)
协程之所以快是因为遇到I/O操作就切换
- 协程的缺点
协程的本质是个单线程,不能同时利用多核心cpu的资源
协程本质上是个单进程,协程相对于多进程来说,无需线程上下文切换的开销,无需原子操作锁定及同步的开销,编程模型也非常简单
yield
yield可以使得函数阻塞,next,和send可以解除阻塞,实现数据不竞争的生产者消费者模式
类似协程思想,但是无法做到在io阻塞时自动切换
- 协程的生产者消费者模式
from time import sleep
def consumer():
'''
消费者
'''
while 1:
num = yield
print('[%s]消费了:%s' % ('consumer',num))
def producer():
'''
生产者
'''
g = consumer()
next(g) # 启动生成器,先初始化第一条数据
for var in range(5):
print('[%s]生产了:%s' % ('producer',var))
g.send(var)
sleep(0.5)
if __name__ == '__main__':
producer()
yield模拟协程,可以同时有多个任务函数以生成器的方式错位执行,但是效率的话不一定比单线程会快,因为多函数切换也会消耗资源,并且这样的函数无法在io阻塞时候自动切换
Gevent
Gevent是一个第三方库,可以轻松通过gevent实现并发同步或异步编程
在gevent中用到的主要模式是Greenlet,它是以C扩展模块形式接入Python的轻量级协程
Gevent对Greenlet又进行了封装,可以自动进行io切换
Greenlet全部运行在主程序操作系统进程的内部,但他们被协作式地调度
- 参考代码
import gevent
import requests
from gevent import monkey
monkey.patch_all() # 为当前作用域下函数模块进行协程的补丁加持
def fetch_async(method, url, req_kwargs):
try:
response = requests.request(method=method, url=url, timeout=(3,3))
print('[%s]:%s'% (url, response.status_code))
except Exception as e:
print('[%s]:%s'% (url, str(e)))
gevent.joinall([
gevent.spawn(fetch_async, method='get', url='https://www.python.org/', req_kwargs={}),
gevent.spawn(fetch_async, method='get', url='https://www.google.com/', req_kwargs={}),
gevent.spawn(fetch_async, method='get', url='https://lienze.tech/', req_kwargs={}),
])
AsyncIO
说到实现协程,就不得不说asyncio模块
asyncio是Python3.4之后的协程模块,使用async/await语法,是目前多种Python协程异步框架首选的异步实现方法
协程可以(摘自官方):
- 等待一个
future结束,这个可以暂时理解为一个即将/正在执行过程的协程方法 - 等待另一个协程(产生一个结果,或引发一个异常)
- 产生一个结果给正在等它的协程
- 引发一个异常给正在等它的协程
基本概念
要理解asyncio的原理, 需要理解如下几个概念: 协程、事件循环、future/task
其中协程就是用户自己定义的任务,事件循环负责监听事件和回调,future/task则主要负责管理回调,以及驱动协程
事件循环所做的全部工作就是等待事件发生,然后再将每个事件与我们已明确与所述事件类型匹配的函数进行匹配
EventLoop
EventLoop事件循环负责同时对多个事件进行监听,当监听到事件时,就调用对应的回调函数,进而驱动不同的任务
比如接下来要对协程进行调用的asyncio.run,其本质就是创建一个事件循环,然后一直运行事件循环,直到加入这个循环中的所有任务结束为止
Task
是Python中与事件循环进行交互的一种主要方式,创建Task,意思就是把协程封装成Task实例,并追踪协程的运行/完成状态,用于未来获取协程的结果
Task的核心作用是为了创建多个可以并发执行的任务
通过asyncio.create_task()创建Task,其次调用实现并发
async def func(i):
print('start-%s' % i)
await asyncio.sleep(1)
print('over-%s' % i)
async def main():
t1 = asyncio.create_task(func(1))
t2 = asyncio.create_task(func(2))
await t1
await t2
asyncio.run(main())
""" 输出
start-1
start-2
over-1
over-2
"""
Future
Future,又称未来对象、期程对象,其本质上是一个容器,用于接受异步执行的结果,是线程不安全的
而之前的Task属于继承自Future,Future 相较于 Task 属于更底层的概念,在开发过程中用到的并不多
Furture 对象内部封装了一个_state,这个_state 维护着四种状态:Pending、Running、Done,Cancelled
如果变成Done完成,就不再等待,而是往后执行,事件循环凭借着四种状态(阻塞、运行态、完成、取消)对Future协程对象进行调度
awaitable
如果一个对象可以在await语句中使用,那么它就是可等待(awaitable)对象;许多asyncio API都被设计为接受可等待对象
可等待对象有三种主要类型: 协程、Task和Future.
编写协程
asyncio通过async与await进行协程方法的编写以及调用
协程的定义,需要使用
async def语句
import asyncio
async def function():
print('这是一个协程方法')
return 1
async def main():
await function()
return 0
验证某个方法是否为协程,可以通过asyncio.iscoroutinefunction(main)
>>> asyncio.iscoroutinefunction(main)
>>> True
调用协程
直接对协程进行函数调用,不会让其真正的调用,而是返回一个coroutine对象,即协程,这一点与生成器函数类似
可以通过如下方法进行协成任务的执行调用
asyncio.run: 这个方法经常用来执行最高层级的类似上面的main这样的起点协程方法
import asyncio
...
asyncio.run(main())
await: 在另一个已经运行的协程中用await等待它,await语句必须在async方法中使用
import asyncio
...
async def main():
await function() # 这里
return 0
并发调用协程
awaitable asyncio.gather(*aws, loop=None, return_exceptions=False)
并发运行aws序列中的协程、tasks、future,如果return_exceptions为True,异常会和成功的结果一样处理,并聚合至结果列表
首先设计一个函数用以输出当前时间
import datetime
def current_time():
# 获取当前时间
return datetime.datetime.now().strftime("%H:%M:%S")
接着是一个用以批量创建的协程方法,非常简单,只是用来输出运行、执行结束、及返回
在执行过程,还添加了一个休眠时间进行阻塞
async def func(sleep_time):
print(f"[{current_time()}] 执行函数 {func.__name__}-{sleep_time}")
await asyncio.sleep(sleep_time)
print(f"[{current_time()}] 执行完毕 {func.__name__}-{sleep_time} ")
return f"函数 {func.__name__}-{sleep_time}"
如果是一个个函数非并发执行结束,那么打印效果,可能类似如下
[11:15:32] 执行异步函数 func-0
[11:15:32] 函数 func-0 执行完毕
[11:15:32] 执行异步函数 func-1
[11:15:33] 函数 func-1 执行完毕
...
创建一定数量的func对应的协程
async def run():
task_list = []
for i in range(5):
task = asyncio.create_task(func(i))
task_list.append(task)
done = await asyncio.gather(*task_list) # 并发调用
print(done) # 输出结果
执行
def main():
asyncio.run(run())
if __name__ == '__main__':
main()
并发的执行后,结果是这样的
[11:30:54] 执行函数 func-0
[11:30:54] 执行函数 func-1
[11:30:54] 执行函数 func-2
[11:30:54] 执行函数 func-3
[11:30:54] 执行函数 func-4
[11:30:54] 执行完毕 func-0
[11:30:55] 执行完毕 func-1
[11:30:56] 执行完毕 func-2
[11:30:57] 执行完毕 func-3
[11:30:58] 执行完毕 func-4
['函数 func-0', '函数 func-1', '函数 func-2', '函数 func-3', '函数 func-4']
等待并发
coroutine asyncio.wait(aws, *, loop=None, timeout=None, return_when=ALL_COMPLETED)
asyncio.wait同样可以并发执行aws序列中的可等待对象,当满足return_when参数指定条件时,将会返回
timeout: [float/int],则它将被用于控制返回之前等待的最长秒数
return_when: 返回的条件
FIRST_COMPLETED: 函数将在任意可等待对象结束或取消时返回FIRST_EXCEPTION: 函数将在任意可等待对象因引发异常而结束时返回ALL_COMPLETED: 函数将在所有可等待对象结束或取消时返回
返回结果为两个集合,分别代表着执行结束、阻塞
还是上一个示例所用到的一些基本函数
并发的函数
import asyncio
async def func(sleep_time):
print(f"[{current_time()}] 执行函数 {func.__name__}-{sleep_time}")
await asyncio.sleep(sleep_time)
print(f"[{current_time()}] 执行完毕 {func.__name__}-{sleep_time} ")
async def run():
task_list = []
for i in range(2):
task = asyncio.create_task(func(i))
task_list.append(task)
done, pending = await asyncio.wait(task_list, return_when=FIRST_COMPLETED)
print(done)
print(pending)
def main():
asyncio.run(run())
if __name__ == '__main__':
main()
当第一个任务执行结束之后,由于FIRST_COMPLETED的设置,已经可以拿到done、pending结果
最终的输出如下
[12:01:57] 执行函数 func-0
[12:01:57] 执行函数 func-1
[12:01:57] 执行完毕 func-0
{<Task finished name='Task-2' coro=<func() done, defined at /Users/zege/Desktop/1.py:11> result='函数 func-0'>}
{<Task pending name='Task-3' coro=<func() running at /Users/zege/Desktop/1.py:13> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x10b1fc670>()]>>}