python 异步编程

来源:

https://download.csdn.net/course/detail/31257?utm_source=coderacademy

协程与任务 — Python 3.10.6 文档 

1、协程

协程(Coroutine),也可以称为线程,是一种用户态内的上下文切换技术,就是通过一个线程实现代码块相互切换执行。

实现协程的方法:

  • greenlet
  • yeild关键字
  • asyncio装饰器(python3.4之后的版本)
  • async、await关键字(python3.5)【推荐】

同步与异步:同步需要等待IO返回的结果,异步不需要IO返回的结果

阻塞与非阻塞:阻塞 程序要等待,非阻塞 程序做其他任务

效果比较:

同步阻塞 = 异步阻塞 < 同步非阻塞 < 异步非阻塞

并发与并行:

并发:协程, 多个任务同时进行, 并行:线程、进程,同一时间做多个任务

1)greenlet实现协程(手动切换)

pip install greenlet

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

def func2():
    print(3)    # 第4步:输出3
    gr1.switch()    # 第5步:切换到 func1函数,从上一次执行的位置继续向后执行
    print(4)    # 第8步:输出4

gr1=greenlet(func1)
gr2=greenlet(func2)

gr1.switch()    # 第1步:去执行 func1函数

 python 异步编程_第1张图片

2)yield关键字(手动切换)

(生成器)

# 生成器函数
def func1():
    yield 1
    yield from func2()    # 跳到func2()
    yield 2

def func2():
    yield 3
    yield 4

f1 = func1()    # 执行了生成器函数func1(),返回了一个生成器f1
for item in f1:    # 对生成器进行循环
    print(item)

python 异步编程_第2张图片

asyncio 

asyncio 是用来编写 并发 代码的库,使用 async/await 语法。

asyncio 被用作多个提供高性能 Python 异步框架的基础,包括网络和网站服务,数据库连接库,分布式任务队列等等。

asyncio 往往是构建 IO 密集型和高层级 结构化 网络代码的最佳选择。

asyncio 提供一组 高层级 API 用于:

  • 并发地 运行 Python 协程 并对其执行过程实现完全控制;
  • 执行 网络 IO 和 IPC;
  • 控制 子进程;
  • 通过 队列 实现分布式任务;
  • 同步 并发代码;

此外,还有一些 低层级 API 以支持 库和框架的开发者 实现:

  • 创建和管理 事件循环,以提供异步 API 用于 网络化, 运行 子进程,处理 OS 信号 等等;
  • 使用 transports 实现高效率协议;
  • 通过 async/await 语法 桥接 基于回调的库和代码。 

asyncio.run() 函数用来运行最高层级的入口点 "main()" 函数。

asyncio.create_task() 函数用来并发运行作为 asyncio 任务 的多个协程。

如果一个对象可以在 await 语句中使用,那么它就是 可等待 对象。

许多 asyncio API 都被设计为接受可等待对象。

可等待 对象有三种主要类型: 协程, 任务 和 Future。

协程属于 可等待 对象,因此可以在其他协程中被等待.

"协程" 可用来表示两个紧密关联的概念:

协程函数: 定义形式为 async def 的函数;

协程对象: 调用 协程函数 所返回的对象。

任务 被用来“并行的”调度协程。

当一个协程通过 asyncio.create_task() 等函数被封装为一个 任务,该协程会被自动调度执行。

Future 是一种特殊的 低层级 可等待对象,表示一个异步操作的 最终结果。

当一个 Future 对象 被等待,这意味着协程将保持等待直到该 Future 对象在其他地方操作完毕。

在 asyncio 中需要 Future 对象以便允许通过 async/await 使用基于回调的代码。

通常情况下 没有必要 在应用层级的代码中创建 Future 对象。

Future 对象有时会由库和某些 asyncio API 暴露给用户,用作可等待对象。

一个很好的返回对象的低层级函数的示例是 loop.run_in_executor()。

 3)asyncio(遇到IO阻塞自动切换)

(用装饰器)

import asyncio
# func1()函数 + coroutine协程 = 协程函数
@asyncio.coroutine
def func1():
    print(1)
    # 网络IO请求:下载一张图片 
    yield from asyncio.sleep(2)    # (等2秒)遇到IO耗时操作,(等待的过程中)自动化切换到tasks中的其他任务
    print(2)

@asyncio.coroutine
def func2():
    print(3)
    # 网络IO请求:下载一张图片 
    yield from asyncio.sleep(2)    # 遇到IO耗时操作,自动化切换到tasks中的其他任务
    print(4)

tasks=[
    asyncio.ensure_future(func1()),    # 相当于ensure_future封装了func1和func2
    asyncio.ensure_future(func2())
]

loop=asyncio.get_event_loop()    # 同时执行一个tasks列表(两个协程函数)
loop.run_until_complete(asyncio.wait(tasks))

python 异步编程_第3张图片

 4)async & await关键字

(不用装饰器了)

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))

python 异步编程_第4张图片  

2、协程的意义

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

下载三张图片(网络IO)

普通方式request(同步):一个执行完再执行下一个,一个一个执行完。

import requests
def download_image(url):
    print("开始下载:",url)
    # 发送网络请求
    response=requests.get(url)
    print("下载成功")
    # 图片保存到本地文件
    file_name=url.rsplit('_')[-1]
    with open(file_name,mode='wb') as file_object:
        file_object.write(response.content)

if __name__=='__main__':
    url_list=[
        'https://www.xxxx/xxx.jpg',
        'https://xxxxx.jpg',
        'https://xxx/xxxxxxx.jpg'
    ]
    for item in url_list:
        download_image(item)

协程方式(异步):用asyncio完成的协程,先把第一个请求发过去,不等结果,去执行下一个任务。

asyncio协程完整的工作流程:

  •     定义/创建协程对象
  •     将协程转为task任务
  •     定义事件循环对象容器
  •     将task任务扔进事件循环对象中触发
# 下载图片用第三方模块aiohttp
import asyncio
import aiohttp

# 定义两个协程函数
async def fetch(session,url):
    print("发送请求",url)
    async with session.get(url,verify_ssl=False) as response:    # 先发请求再去下载url
        content=await response.content.read()
        file_name=url.split('_')[-1]
        with open(file_name,mode='wb') as file_object:
            file_object.write(content)
        print("下载完成:",url)
async def main():
    async with aiohttp.ClientSession() as session:
        url_list = [
            'https://www.xxxxxxx/xxx.jpg',
            'https://xxxx/xxxxxxx.jpg',
            'https://xxxxxxxxxxx/xxxxxxx.jpg'
        ]
        # 下载就有IO请求,就有等待,等待时发送第二个url请求
        tasks=[asyncio.creat_task(fetch(session,url)) for url in url_list]
        await asyncio.wait(tasks)

if __name__=='__main__':
    asyncio.run(main())

3、异步编程

async def 用来定义异步函数,其内部有异步操作。每个线程有一个事件循环,主线程调用asyncio.get_event_loop()时会创建事件循环,你需要把异步的任务丢给这个循环的run_until_complete()方法,事件循环会安排协同程序的执行。

运行 asyncio 程序

asyncio.run(coro, *, debug=False)此函数会运行传入的协程,负责管理 asyncio 事件循环,终结异步生成器,并关闭线程池。 当有其他 asyncio 事件循环在同一线程中运行时,此函数不能被调用。 如果 debug 为 True,事件循环将以调试模式运行。 此函数总是会创建一个新的事件循环并在结束时关闭之。它应当被用作 asyncio 程序的主入口点,理想情况下应当只被调用一次。

创建任务

asyncio.create_task(coro, *, name=None) 将 coro 协程 封装为一个 Task 并调度其执行。返回 Task 对象。 name 不为 None,它将使用 Task.set_name() 来设为任务的名称。 该任务会在 get_running_loop() 返回的循环中执行,如果当前线程没有在运行的循环则会引发 RuntimeError。 保存一个指向此函数的结果的引用,以避免任务在执行过程中消失。 事件循环将只保留对任务的弱引用。 未在其他地方被引用的任务可能在任何时候被作为垃圾回收,即使是在它完成之前。 如果需要可靠的“发射后不用管”后台任务,请将它们收集到一个多项集中。

休眠

coroutine asyncio.sleep(delay, result=None) 阻塞 delay 指定的秒数。 如果指定了 result,则当协程完成时将其返回给调用者。 sleep() 总是会挂起当前任务,以允许其他任务运行。 将 delay 设为 0 将提供一个经优化的路径以允许其他任务运行。 这可供长期间运行的函数使用以避免在函数调用的全过程中阻塞事件循环。

并发运行任务

awaitable asyncio.gather(*aws, return_exceptions=False) 并发 运行 aws 序列中的 可等待对象。 如果 aws 中的某个可等待对象为协程,它将自动被作为一个任务调度。 如果所有可等待对象都成功完成,结果将是一个由所有返回值聚合而成的列表。结果值的顺序与 aws 中可等待对象的顺序一致。 如果 return_exceptions 为 False (默认),所引发的首个异常会立即传播给等待 gather() 的任务。aws 序列中的其他可等待对象 不会被取消 并将继续运行。 如果 return_exceptions 为 True,异常会和成功的结果一样处理,并聚合至结果列表。 如果 gather() 被取消,所有被提交 (尚未完成) 的可等待对象也会 被取消。 如果 aws 序列中的任一 Task 或 Future 对象 被取消,它将被当作引发了 CancelledError 一样处理 -- 在此情况下 gather() 调用 不会 被取消。这是为了防止一个已提交的 Task/Future 被取消导致其他 Tasks/Future 也被取消。如果 return_exceptions 为 False,则在 gather() 被标记为已完成后取消它将不会取消任何已提交的可等待对象。 例如,在将一个异常传播给调用者之后,gather 可被标记为已完成,因此,在从 gather 捕获一个(由可等待对象所引发的)异常之后调用 gather.cancel() 将不会取消任何其他可等待对象。 在 3.10 版更改: 移除了 loop 形参。

Task 对象

class asyncio.Task(coro, *, loop=None, name=None) 一个与 Future 类似 的对象,可运行 Python 协程。非线程安全。 Task 对象被用来在事件循环中运行协程。如果一个协程在等待一个 Future 对象,Task 对象会挂起该协程的执行并等待该 Future 对象完成。当该 Future 对象 完成,被打包的协程将恢复执行。 事件循环使用协同日程调度: 一个事件循环每次运行一个 Task 对象。而一个 Task 对象会等待一个 Future 对象完成,该事件循环会运行其他 Task、回调或执行 IO 操作。 使用高层级的 asyncio.create_task() 函数来创建 Task 对象,也可用低层级的 loop.create_task() 或 ensure_future() 函数。不建议手动实例化 Task 对象。

1)asyncio事件循环

理解成为一个死循环,去检测列表的任务是否可执行,并执行某些代码。可执行的就执行;不可执行就检查不到就忽略掉,不处理,就一直等待IO请求。如果IO请求已经处理完了,任务就变成可执行了。当IO完成之后,再去执行这个函数。

#伪代码
任务列表 = [任务1,任务2,任务3,...]


while True:
        可执行的任务列表,已完成的任务列表 = 去任务列表中检查所有的任务,将'可执行'和'已完成'的任务返回


        for 就绪任务 in 已准备就绪的任务列表:
                执行已就绪的任务


        for 已完成的任务 in 已完成的任务列表:
                在任务列表中移除已完成的任务


        如果任务列表中的任务都已完成,则终止循环 

import asyncio

tasks=[ ]
# 去生成或获取一个事件循环while/for循环
loop=asyncio.get_event_loop()   
# 将任务放到任务列表,去检测列表的任务是否可执行loop.run_until_complete(任务)
loop.run_until_complete(asyncio.wait(tasks))    
  • 协程函数:定义函数时候,async def 函数名。
  • 协程对象:执行 协程函数() 得到的协程对象。
async def func():
    pass
result = func()
# 执行协程函数创建协程对象,函数内部代码不会执行
# 如果想要运行协程函数内部代码,必须要将协程对象交给事件循环来处理
import asyncio
async def func():
    print("AAAAAA")

# 以下两种写法二选一

# loop=asyncio.get_event_loop()
# loop.run_until_complete(func())

asyncio.run(func())    # 适用于python3.7以后

2)await

await+可等待的对象-> IO等待

可等待的对象:协程对象、Future对象、Task对象

import asyncio
async def func():
    print("AAAA")
    # 如果有其它任务,等待期间去做其他的,其他执行完了,或者io等待时间已经完成了结果回来了,这个事件内部就会在下一次检测到已经完成了,再去往下执行。
    # await关键字 + io等待时间(网络请求)。
    response = await asyncio.sleep(2)    
    print("结束:",response)
    
asyncio.run(func())

python 异步编程_第5张图片

asyncio.run(协程对象),run加到协程对象相当于是一个任务,把这个任务加到事件循环里面去了,事件循环就会执行这个函数内容

import asyncio
async def others():
    print("AAAA")
    await asyncio.sleep(2)
    print("end")
    return '返回值'
async def func():
    print("执行协程函数内部代码")
    # 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行,当前协程挂起时,时间循环可以去执行其他协程(任务)
    response = await others()
    print("IO请求结束,结果为:",response)
# loop=asyncio.get_event_loop()
# loop.run_until_complete(func())
asyncio.run(func())

python 异步编程_第6张图片

 await就是等待对象的值得到结果之后再继续向下走。

import asyncio
async def others():
    print("start")
    await asyncio.sleep(2)
    print("end")
    return '返回值'
async def func():
    print("执行协程函数内部代码")
    # 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行,当前协程挂起时,时间循环可以去执行其他协程(任务)
    response1 = await others()
    print("IO请求结束,结果为:",response1)
    response2 = await others()
    print("IO请求结束,结果为:",response2)
loop=asyncio.get_event_loop()
loop.run_until_complete(func())
# asyncio.run(func())

python 异步编程_第7张图片

Task创建多个任务对象,需要使用Task对象来实现。
Tasks用于并发调度协程,通过asyncio.create_task(协程对象)的方式创建Task对象,这样可以让协程加入事件循环中等待被调度执行。除了使用 asyncio.create_task() 函数以外,还可以用低层级的 loop.create_task() 或 ensure_future() 函数。不建议手动实例化 Task 对象。

本质上是将协程对象封装成task对象,并将协程立即加入事件循环,同时追踪协程的状态。
 

import asyncio
async def func():
    print(1)
    await asyncio.sleep(2)
    print(2)
    return "返回值"
async def main():
    print("main开始")
    # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
    task1 = asyncio.create_task(func())
    # 创建协程,将协程封装到一个Task对象中并立即添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
    task2 = asyncio.create_task(func())
    print("main结束")
    # 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
    # 此处的await是等待相对应的协程全都执行完毕并获取结果
    ret1 = await task1
    ret2 = await task2
    print(ret1, ret2)
loop=asyncio.get_event_loop()
loop.run_until_complete(main())
import asyncio
async def func():
    print(1)
    await asyncio.sleep(2)
    print(2)
    return "返回值"
async def main():
    print("main开始")
    # 创建协程,将协程封装到Task对象中并添加到事件循环的任务列表中,等待事件循环去执行(默认是就绪状态)。
    # 在调用
    task_list = [
        asyncio.create_task(func(), name="n1"),
        asyncio.create_task(func(), name="n2")
    ]
    print("main结束")
    # 当执行某协程遇到IO操作时,会自动化切换执行其他任务。
    # 此处的await是等待所有协程执行完毕,并将所有协程的返回值保存到done
    # 如果设置了timeout值,则意味着此处最多等待的秒,完成的协程返回值写入到done中,未完成则写到pending中。
    done, pending = await asyncio.wait(task_list, timeout=None)
    print(done, pending)
asyncio.run(main())
# asyncio.wait 源码内部会对列表中的每个协程执行ensure_future从而封装为Task对象,所以在和wait配合使用时task_list的值为[func(),func()] 也是可以的。
import asyncio
async def func():
    print("执行协程函数内部代码")
    # 遇到IO操作挂起当前协程(任务),等IO操作完成之后再继续往下执行。当前协程挂起时,事件循环可以去执行其他协程(任务)。
    response = await asyncio.sleep(2)
    print("IO请求结束,结果为:", response)
coroutine_list = [func(), func()]
# 错误:coroutine_list = [ asyncio.create_task(func()), asyncio.create_task(func()) ] 
# 此处不能直接 asyncio.create_task,因为将Task立即加入到事件循环的任务列表,
# 但此时事件循环还未创建,所以会报错。
# 使用asyncio.wait将列表封装为一个协程,并调用asyncio.run实现执行两个协程
# asyncio.wait内部会对列表中的每个协程执行ensure_future,封装为Task对象。
done,pending = asyncio.run( asyncio.wait(coroutine_list) )

futures.Future对象

在Python的concurrent.futures模块中也有一个Future对象,这个对象是基于线程池和进程池实现异步操作时使用的对象。

import time
from concurrent.futures import Future
from concurrent.futures.thread import ThreadPoolExecutor
from concurrent.futures.process import ProcessPoolExecutor
def func(value):
    time.sleep(1)
    print(value)
pool = ThreadPoolExecutor(max_workers=5)    # 创建线程池
# 或 pool = ProcessPoolExecutor(max_workers=5)    # 创建进程池
for i in range(10):
    fut = pool.submit(func, i)
    print(fut)

两个Future对象是不同的,他们是为不同的应用场景而设计,例如:concurrent.futures.Future不支持await语法 等。
在Python提供了一个将futures.Future 对象包装成asyncio.Future对象的函数 asynic.wrap_future。

其实,一般在程序开发中我们要么统一使用 asycio 的协程实现异步操作、要么都使用进程池和线程池实现异步操作。但如果 协程的异步和 进程池/线程池的异步 混搭时,那么就会用到此功能了。

当项目以协程式的异步编程开发时,如果要使用一个第三方模块,而第三方模块不支持协程方式异步编程时,就需要用到这个功能。

import asyncio
import requests
async def download_image(url):
    # 发送网络请求,下载图片(遇到网络下载图片的IO请求,自动化切换到其他任务)
    print("开始下载:", url)
    loop = asyncio.get_event_loop()
    # requests模块默认不支持异步操作,所以就使用线程池来配合实现了。
    future = loop.run_in_executor(None, requests.get, url)
    response = await future
    print('下载完成')
    # 图片保存到本地文件
    file_name = url.rsplit('_')[-1]
    with open(file_name, mode='wb') as file_object:
        file_object.write(response.content)
if __name__ == '__main__':
    url_list = [
        'https://621.jpg',
        'https://0429.jpg',
        'https:/193.jpg'
    ]
    tasks = [download_image(url) for url in url_list]
    loop = asyncio.get_event_loop()
    loop.run_until_complete( asyncio.wait(tasks) )

python 异步编程_第8张图片

 

异步迭代器

什么是异步迭代器

实现了 __aiter__() 和 __anext__() 方法的对象。__anext__ 必须返回一个 awaitable 对象。async for 会处理异步迭代器的 __anext__() 方法所返回的可等待对象,直到其引发一个 StopAsyncIteration 异常。

什么是异步可迭代对象?

可在 async for 语句中被使用的对象。必须通过它的 __aiter__() 方法返回一个 asynchronous iterator。

import asyncio
class Reader(object):
    """ 自定义异步迭代器(同时也是异步可迭代对象) """
    def __init__(self):
        self.count = 0
    async def readline(self):
        # await asyncio.sleep(1)
        self.count += 1
        if self.count == 100:
            return None
        return self.count
    def __aiter__(self):
        return self
    async def __anext__(self):
        val = await self.readline()
        if val == None:
            raise StopAsyncIteration
        return val
async def func():
    # 创建异步可迭代对象
    async_iter = Reader()
    # async for 必须要放在async def函数内,否则语法错误。
    async for item in async_iter:
        print(item)
asyncio.run(func())

异步迭代器其实没什么太大的作用,只是支持了async for语法而已。

异步上下文管理器

此种对象通过定义 __aenter__() 和 __aexit__() 方法来对 async with 语句中的环境进行控制。

import asyncio
class AsyncContextManager:
    def __init__(self):
        self.conn = conn
    async def do_something(self):
        # 异步操作数据库
        return 666
    async def __aenter__(self):
        # 异步链接数据库
        self.conn = await asyncio.sleep(1)
        return self
    async def __aexit__(self, exc_type, exc, tb):
        # 异步关闭数据库链接
        await asyncio.sleep(1)
async def func():
    async with AsyncContextManager() as f:
        result = await f.do_something()
        print(result)
asyncio.run(func())

异步的上下文管理器在打开、处理、关闭 操作时,就可以用。async和await关键字,是基于协程实现的异步编程,这种异步编程是通过一个线程在IO等待时间去执行其他任务,从而实现并发。

uvloop

Python标准库中提供了asyncio模块,用于支持基于协程的异步编程。

uvloop是 asyncio 中的事件循环的替代方案,替换后可以使得asyncio性能提高。事实上,uvloop要比nodejs、gevent等其他python异步框架至少要快2倍,性能可以比肩Go语言。

安装
pip3 install uvloop

import asyncio
import uvloop
asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())
# 编写asyncio的代码,与之前写的代码一致。
# 内部的事件循环自动化会变为uvloop
asyncio.run(...)

python 3.7 以前的版本调用异步函数的步骤:

调用asyncio.get_event_loop()函数获取事件循环loop对象

通过不同的策略调用loop.run_forever()方法或者loop.run_until_complete()方法执行异步函数

python3.7 以后的版本使用asyncio.run即可。此函数总是会创建一个新的事件循环并在结束时关闭之。它应当被用作 asyncio 程序的主入口点,理想情况下应当只被调用一次。

你可能感兴趣的:(python)