Python asyncio异步编程

820 阅读7分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第23天,点击查看活动详

1. 协程

协程不是计算机提供,是程序员人为创造的概念。

协程简而言之就是通过一个线程实现代码块相互切换执行。

协程实现了单线程并发,属于异步编程

实现协程的方式有以下几种方法:

  • greenlet gevent
  • yield
  • asyncio装饰器(python3.4)
  • async await关键字(python3.5版本之后,推荐使用方式)

1.1 greenlet实现协程

pip install greenlet

from greenlet import greenlet
​
​
def func1():
    print(1)  # 2 输出1
    gr2.switch()  # 3 执行func2
    print(2)  # 6 输入2
    gr2.switch()  # 7 切换到func2,从上一次执行位置继续向后执行
    
    
def func2():
    print(3)  # 4 输出3
    gr1.switch()  # 5 切换到func1函数,从上次执行的位置继续向后执行
    print(4)  # 8 输出4
    
​
gr1 = greenlet(func1)
gr2 = greenlet(func2)
​
gr1.switch()  # 1 执行func1函数

1.2 yield关键字实现协程(了解)

def func1():
    yield 1
    yield from func2()
    yield 2
    
def fucn2():
    yield 3
    yield 4
    
f = func1()
for i in f:
    print(i)  # 1 3 4 2

1.3 asyncio实现协程

遇到IO阻塞,asyncio会自动切换

import asyncio
​
​
@asyncio.coroutine  # 3.8弃用了
def func1():
    print(1)
    yield from asyncio.sleep(2)  # 遇到IO耗时操作时会自动切换到tasks中的其他任务
    print(2)
​
​
@asyncio.coroutine
def func2():
    print(3)
    yield from asyncio.sleep(2)
    print(4)
​
​
tasks = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2()),
]
​
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))
​

1.5 async和await实现协程(python3.5以后)

推荐使用

import asyncio
​
​
async def func1():
    print(1)
    await asyncio.sleep(2)
    print(2)
​
​
async def func2():
    print(3)
    await asyncio.sleep(2)
    print(4)
​
​
tasks = [
    asyncio.ensure_future(func1()),
    asyncio.ensure_future(func2()),
]
​
loop = asyncio.get_event_loop()
loop.run_until_complete(asyncio.wait(tasks))

1.6 协程的意义

在一个线程中如果遇到IO等待,线程不会等待,利用空闲的时间再去做一些其他事情。

2 异步编程

2.1 事件循环

事件循环可以理解为一个while循环,在这个循环中检测并执行一些代码。

事件循环会检查任务列表,对不同状态的任务进行不同的处理:比如一个任务是可以被执行的,那么就会执行该任务,如果一个任务处于IO状态,就是不可以被执行的状态,那么事件循环就会忽略这个任务,如果一个任务已经被执行完成,这个任务就会在任务列表中被删除,直到所有任务完成,事件循环也会随之结束。

任务列表 = [ task1, task2 task3, ...]
while True:
    可执行的任务列表 = 任务列表中检查可以被执行的任务
    已完成的任务列表 = 任务列表中检查已经执行完成的任务
    
    for 可执行任务 in 可执行任务列表:
        执行任务
    
    for 已完成任务 in 已完成任务列表:
        任务列表中删除已完成任务
    
    如果任务列表中所有的任务都完成,就终止循环
import asyncio
​
# 生成一个事件循环
loop = asyncio.get_event_loop()
# 将任务放到任务列表中,并执行事件循环
loop.run_until_complete(任务)

2.2 快速上手

协程函数:async def 函数函数名()

协程对象:执行 协程函数() 得到协程对象

# 协程函数
async def func():
    pass# 协程对象
xc = func()
注意:执行协程函数创建协程对象时,函数内部代码不会执行

如何运行协程函数中的代码?就需要协程对象和事件循环结合使用。

即如果想要运行协程函数内部代码,必须要将协程对象交给时间循环来处理

async def func():
    print("代码执行")
    
res = func()  # 得到协程对象
​
loop = asyncio.get_event_loop()
loop.run_until_complete(res)
​
asyncio.run(res)  # python3.7以后可以使用一行代码代替上述两行代码,原理就是上面事件循环的两行代码

2.3 await

await就是等到await之后的对象执行完成之后再继续向下走,当下一步需要上一步的结果时需要使用。

遇到IO就加上await,用于遇到IO时挂起当前任务,当前任务挂起之后,事件循环可以去执行其他任务

await+可等待对象,可等待的对象包括协程对象、Future、Task对象

import asyncio
​
​
async def func1():
    print(1)
    response = await func2()  
    print(2)
​
​
async def func2():
    print(3)
    await asyncio.sleep(2)
    print(4)
    return "func2"
​
asyncio.run(func1())

2.4 Task对象

在事件循环中添加多个任务,并且实现遇到IO进行任务切换的操作。

Task用于并发调度协程,通过asyncio.create_task(协程对象)创建task并且将task加到事件循环中。除了使用create_task可以创建task对象,也可以使用更加底层的方法loop.create_task协程对象()或者ensure_future(协程对象)创建task。

asyncio.create_task(协程对象)在Python3.7及以后版本可以使用,在3.7版本之前使用ensure_future(协程对象)创建task对象。

import asyncio
​
​
async def func1():
    print(1)
    await asyncio.sleep(2)
    print(2)
​
​
async def func2():
    print("start")
    task1 = asyncio.create_task(func1())
    task2 = asyncio.create_task(func1())
    print("end")
    
    res1 = await task1
    res2 = await task2
    print(res1, res2)
​
asyncio.run(func2())
​

通常事件循环和多个任务结合使用的时候,会使用如下方式编写代码:

import asyncio
​
​
async def func1():
    print(1)
    await asyncio.sleep(2)
    print(2)
​
​
async def func2():
    print("start") 
    task_list = [
        asyncio.create_task(func1(), name='t1'),  # name给任务起一个别名
        asyncio.create_task(func1())
    ]
    print("end")
    # done是协程对象的返回值集合
    done, pending = await asyncio.wait(task_list)
    print(res1, res2)
​
asyncio.run(func2())

还可以使用下述方式:

import asyncio
​
​
async def func1():
    print(1)
    await asyncio.sleep(2)
    print(2)
​
'''
直接这样写会报错,因为create_task是立刻将协程对象添加到事件循环中,但是此时事件循环还未创建
task_list = [
        asyncio.create_task(func1(), name='t1'),  # name给任务起一个别名
        asyncio.create_task(func1())
    ]
'''
task_list = [
       func1(),  # name给任务起一个别名
       func1()
    ]
​
asyncio.run(asyncio.wait(task_list))  

2.5 asyncio.Future对象

Task继承了Future,Task对象内部await结果的处理就是基于Future对象

import asyncio
​
​
async def func1():
    print(1)
    loop = asyncio.get_running_loop()  # 获取事件循环
    fu = loop.create_future()  # 创建一个任务对象,这个任务什么都不敢
    await fu  # 等待任务结果Future对象,如果没有结果会一直等待,表现为卡死
​
​
asyncio.run(func1())
import asyncio
​
​
async def set_after(fu):
    await asyncio.sleep(2)
    fu.set_result("666")
    
    
async def func1():
    print(1)
    loop = asyncio.get_running_loop()  # 获取事件循环
    fu = loop.create_future()  # 创建一个任务对象,这个任务什么都不做
    await loop.create_task(set_after(fu))    # 手动设置future对象的结果
​
​
asyncio.run(func1())

2.6 concurrent.futures.Future对象

线程池的Future对象在某些情况下可能会和asyncio的future对象结合使用,比如某些模块不支持协程异步,如果想要实现异步编程就只能借助线程池或者进程池。

如果想要继续使用事件循环执行协程任务,可以使用loop.run_in_executor方法将concurrent.futures.Future对象包装成asyncio.Future对象。

import time
import async.io
import concurrent.futures
​
def func1(x):
    # 模拟不支持协程操作
    print(x+1)
    await asyncio.sleep(2)
    print(2)
    
async def main():
    loop = asyncio.get_running_loop()
    
    fut = loop.run_in_executor(None, func1, 1)  # None表示创建线程池,func1执行的函数,1是函数的参数
    '''
    1. 首先申请线程池执行func1函数返回concurrent.futures.Future对象
    2. 调用asyncio.wrap_future将concurrent.futures.Future对象包装成asyncio.Future对象
    '''
    
    res = await fut
    
asyncio.run(main)

2.7 异步迭代器

异步迭代器:实现了__aiter____anext__方法

异步可迭代对象:实现了__aiter__方法并且返回异步迭代器

import asyncio
​
​
class Reader:
    
    def __init__(self):
        self.count = 0
        
    async def readline(self):
        self.count += 1
        if self.count == 100:
            return None
        return self.count
    
    def __aiter__(self):
        return self
​
    def __anext__(self):
        val = await self.readline()
        if not val:
            raise StopIteration
        return val
    
    
async def func():
    iteration = Reader()
    # async for必须写在协程函数中
    async for i in iteration: 
        print(i)
        
asyncio.run(func())
​

2.8 异步上下文管理器

通过定义__aenter____aexit__方法对async with语句中的环境进行控制。

import asyncio
​
​
class Reader:
​
    def __init__(self):
        self.conn = None
​
    def __aenter__(self):
        # 模拟异步链接数据库
        self.conn = await asyncio.sleep(2)
        return self
​
    def __aexit__(self, exc_type, exc_val, exc_tb):
        # 异步模拟关闭数据库连接
        await asyncio.sleep(1)
​
    async def do_something(self):
        # 异步操作数据库
        return 666
​
​
async def func():
    # async with必须写在协程函数中
    async with Reader() as f:
        res = await f.do_something()
        print(res)
​
​
asyncio.run(func())

3. uvloop

是asyncio事件循环的替代方案,uvloop的事件循环性能要高于asyncio的事件循环。

uvloop暂时不支持windows环境

pip install uvloop

import asyncio
import uvloop
​
# 使用uvloop的时间循环代替asyncio中的事件循环
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
# 后续代码不变
asyncio.run(...)  # 会事件循环自动变成uvloop

运行fastapi项目的uvicorn内部使用的就是uvloop

4. 异步操作Redis

在使用Python代码操作redis时,链接、操作、断开都是网络IO。

支持网络IO的异步模块是aioredis,需要进行安装,使用的版本是1.3.1,使用最新版的aioredis没有create_redis_pool方法

import asyncio
import aioredis
​
​
async def main():
    redis = await aioredis.create_redis_pool('redis://localhost')
    await redis.set('my-key', 'value')
    value = await redis.get('my-key', encoding='utf-8')
    print(value)
​
    redis.close()
    await redis.wait_closed()
​
asyncio.run(main())
​