python 协程进阶

python 协程实现
python 协程进阶
python 生成器的作用
协程在多个模型流式输出中的使用实例

文章目录

  • 1. 协程基础
    • 1.1. 协程名词解释
    • 1.2. 基本工作流程
    • 1.3. async 协程执行
      • 1.3.1. 协程顺序执行(asyncio.run)
      • 1.3.2. 协程顺序执行(await)
      • 1.3.3. 协程同步执行(asyncio.create_task)
  • 2. 可等待对象(Awaitables)
    • 2.1. Coroutine(协程)
    • 2.2. Task(任务)
      • 2.2.1. Task 对象
      • 2.2.2. cancel()
      • 2.2.3. cancelled()
      • 2.2.4. done()
      • 2.2.5. result()
      • 2.2.6. exception()
      • 2.2.7. add_done_callback()
      • 2.2.8 调试接口
    • 2.3. Future(未来对象)
      • 2.3.1. Future 创建
  • 3. 事件循环 Eventloop
    • 3.1. 事件循环概述
    • 3.2. 事件循环执行原理
    • 3.3. 事件循环执行协程
      • 3.3.1. 协程函数执行(loop.run_until_complete)
      • 3.3.2. 协程函数执行(loop.run_forever)
    • 3.4. 动态添加同步函数
      • 3.4.1. 动态添加同步函数(loop.call_soon_threadsafe)
      • 3.4.2. 同步环境添加同步函数(loop.run_in_executor)
      • 3.4.3. 异步环境添加异步函数(asyncio.run_coroutine_threadsafe)
      • 3.4.4. run_coroutine_threadsafe 和 run_in_executor 的区别
  • 4. 实例
  • 5. 原语列表
    • 5.1. asyncio 原语
      • 5.1.1. asyncio.run(执行程序)
      • 5.1.2. asyncio.create_task(创建对象)
      • 5.1.3. asyncio.ensure_future
      • 5.1.4. asyncio.sleep(Sleeping)
      • 5.1.5. asyncio.gather(协程并发执行)
      • 5.1.6. asyncio.shield(避免取消)
      • 5.1.7. asyncio.wait_for(超时 Timeouts)
      • 5.1.8. asyncio.wait(等待原语)
      • 5.1.9. asyncio.as_completed
      • 5.1.10. asyncio.run_coroutine_threadsafe
      • 5.1.11. asyncio.current_task
      • 5.1.12. asyncio.all_tasks
    • 5.2. loop 原语
      • 5.2.1. loop.run_until_complete(协程函数执行)
      • 5.2.2. loop.run_forever(协程函数执行)
      • 5.2.3. loop.stop
      • 5.2.4. loop.call_soon_threadsafe(动态添加同步函数)
      • 5.2.5. loop.run_in_executor(动态添加同步函数)

协程基础概念:
https://blog.csdn.net/peng78585/article/details/128342847
参考:
https://myoule.zhipin.com/articles/6013ad6d3f93a648qxB43Nu0Fg~~.html
https://blog.csdn.net/weixin_62077901/article/details/126402747

1. 协程基础

1.1. 协程名词解释

async/await 关键字: python3.5 用于定义协程的关键字。async 定义一个协程函数;await 用于挂起阻塞的异步调用接口,在代码块执行过程中,可以将执行权交给其他协程,其作用在一定程度上类似于 yield。
coroutine 对象: 调用协程函数返回的对象,称之为协程对象。当协程函数被调用时,不会立即执行,而是会返回一个协程对象。
task 协程任务: 协程任务是对协程进一步封装,其中包含任务的各种状态。Task 对象是 Future 的子类,它将 coroutine 和 Future 联系在一起,将 coroutine 封装成一个 Future 对象。
future 对象: Future 是一种特殊的底层可等待对象,代表一个异步操作的最终结果。它和 task 上没有本质的区别。
event_loop 事件循环: 程序开启一个无限循环,协程对象需要注册到该事件循环上,由事件循环调用。

1.2. 基本工作流程

  1. 定义并创建协程对象
  2. 定义事件循环对象容器
  3. 将协程转为 task 任务
  4. 事件循环触发 task 任务
import asyncio

# 使用 async def 关键词来定义协程,区别于普通函数使用 def 关键词。
async def test():
    print('Hello World!')
    await asyncio.sleep(1)
    print(f"Resuming a sleep 1s")

if __name__ == "__main__":
    # 1. 创建协程对象
    coro = test()
    print(type(coro), coro)         #  
	
	# code 1
    # result = asyncio.run(coro)
    
    # code 2
    # 2. 定义事件循环
    loop = asyncio.get_event_loop()
    # task = asyncio.ensure_future(coro)

    # 3. 将协程转为 task 任务
    task = loop.create_task(coro)

    # 4. 事件循环触发 task 任务
    loop.run_until_complete(task)

调用 test(),会返回一个 coroutine 对象,test() 方法内的代码不会执行。

1.3. async 协程执行

实际上,asyncio 提供了三种执行协程的机制:

  1. 使用 asyncio.run() 执行协程。一般用于执行最顶层的入口函数,如 test()。
  2. await 一个协程。一般用于在一个协程中调用另一协程。
  3. 使用 asyncio.create_task() 方法将协程封装为任务(Task)。 需要注意的是,只有在当前线程存在事件循环的时候才能创建任务(Task)。

1.3.1. 协程顺序执行(asyncio.run)

python 3.7 之后可以不需要自己获取 loop 对象,可以直接调用 asyncio.run() 方法。该方法负责管理 asyncio 事件循环,终止异步生成器。
注意:

  • 该方法内部已经帮我们获取了 loop 对象和调用 loop.run_until_complete()。
  • 此函数总是创建一个新的事件循环,并在最后关闭它。建议将它用作 asyncio 程序的主入口,并且只调用一次。
  • 不支持同时运行多个协程对象。
  • 在同一个线程中,当已经有 asyncio 事件循环在执行时,不能调用此函数。

调用实例:

import asyncio

async def main():
    print('Hello World!')
    await asyncio.sleep(1)
    print(f"Resuming a sleep 1s")
    return "OK"

if __name__ == "__main__":
    result = asyncio.run(main())
    print(result)

1.3.2. 协程顺序执行(await)

使用 await 关键字来修饰函数的调用,如 result = await testa(2),但是 await 只能用在协程函数中,所以想要用 await 关键字就还需要定义一个协程函数。
并且最终的执行还是需要放到一个事件循环中进行。

import asyncio
import time

async def testa(x):
    print("in testa")
    await asyncio.sleep(x)
    print(f"Resuming a sleep {x}s")
    return x

async def testb(x):
    print("in testb")
    await asyncio.sleep(x)
    print(f"Resuming b sleep {x}s")
    return x

async def main():
    start_time = time.time()
    resulta = await testa(3)
    print(f"test resulta is {resulta}")
    resultb = await testb(1)
    print(f"test resultb is {resultb}")
    end_time = time.time()
    print(f"use time:{end_time-start_time}")
    return "OK"

if __name__ == "__main__":
    # loop = asyncio.get_event_loop()
    # result = loop.run_until_complete(main())
    result = asyncio.run(main())
    print(result)

运行结果:

in testa
Resuming a sleep 3s
test resulta is 3
in testb
Resuming b sleep 1s
test resultb is 1
use time:4.0292747020721436
OK

这段代码定义了两个协程,并将它们放到另外一个协程 main 函数中。
事件循环的特点是当它遇到某个 I/O 需要等待(如这里的 asyncio.sleep() 函数)的时候,可以去执行其它的函数,这样,整个函数执行所需要的时间,应该是所有协程中执行时间最长的那个,对于上面这个代码来说,一个 sleep 了3秒,一个sleep 了1秒,总的用时应该是3秒多一点,但结果却是运行了4秒。
而且是先执行了 testa 函数,然后再执行了 testb 函数,是串行的依次执行的,并没有像我们想象中的并发执行。

1.3.3. 协程同步执行(asyncio.create_task)

我们修改以上的例程,并发执行两个协程。

......
async def main():
    start_time = time.time()
    taska = asyncio.create_task(testa(3))
    taskb = asyncio.create_task(testb(1))
    print(taska)
    print(taskb)
    print(taska.done(), taskb.done())
    
    await taskb
    await taska
    print(taska)
    print(taskb)
    print(taska.done(), taskb.done())
    
    print(taska.result())
    print(taskb.result())
    end_time = time.time()
    print(f"use time:{end_time-start_time}")

运行结果:

<Task pending name='Task-2' coro=<testa() running at D:\works\demo\multi\modelmulti\test.py:4>>
<Task pending name='Task-3' coro=<testb() running at D:\works\demo\multi\modelmulti\test.py:10>>
False False

in testa
in testb
Resuming b sleep 1s
Resuming a sleep 3s
<Task finished name='Task-2' coro=<testa() done, defined at D:\works\demo\multi\modelmulti\test.py:4> result=3>
<Task finished name='Task-3' coro=<testb() done, defined at D:\works\demo\multi\modelmulti\test.py:10> result=1>
True True

3
1
use time:3.020379066467285

总耗时 3s。跟阻塞最长的协程耗时一样。

2. 可等待对象(Awaitables)

如果一个对象能够被用在 await 表达式中,那么我们称这个对象是可等待对象(awaitable object)。很多 asyncio API 都被设计成了可等待的。
主要有三类可等待对象:

  • 协程 coroutine
  • 任务 Task
  • 未来对象 Future

2.1. Coroutine(协程)

Python 的协程是可等待的(awaitable),因此能够被其他协程用在 await 表达式中。

......
async def main():
    ......
    resulta = await testa(3)
    print(f"test resulta is {resulta}")
	......
......

重要: coroutine 或协程指代两个关系紧密的概念:
协程函数(coroutine function):由 async def 定义的函数;
协程对象(coroutine object):调用 协程函数返回的对象。
asyncio 也支持传统的基于生成器的协程。

2.2. Task(任务)

2.2.1. Task 对象

class asyncio.Task(coro,*,loop=None)

类似与 Future 对象,用于执行 Python 协程。非线程安全。
Tasks 用于在事件循环中执行协程。如果协程等待一个 Future,那么 Task 会暂停协程的执行,直到 Future 执行完成。当 Future 完成时,协程的执行会恢复。
事件循环的协作调度模式:一个事件循环同一时间只执行一个Task。当这个 Task 等待某个 Future 返回时,事件循环执行其他的 Task、回调或 IO 操作。

可以通过高层函数 asyncio.create_task() 创建 Task,或者通过底层函数loop.create_task() 和 ensure_future() 创建 Task。但是不建议直接实例化 Task 对象。

如果想要取消一个 Task 的执行,可以使用 cancel() 方法。调用 cancel() 会引起 Task 对象向被封装的协程抛出 CancelledError 异常。当取消行为发生时,如果协程正在等待某个 Future 对象执行,该 Future 对象将被取消。

cancelled() 方法用于检查某个 Task 是否已被取消。如果 Task 封装的协程没有阻止 CancelledError 异常,且 Task 确实被取消了,则该方法返回 True。

asyncio.Task 继承了 Future 类中除 Future.set_result() 和 Future.set_exception() 以外的所有方法。

Task 对象支持 contextvars 模块:当一个 Task 被创建的时候,它会复制当前的上下文,然后在复制的上下文副本中执行协程。

Python3.7 中的变更:添加了对 contextvars 模块的支持。

2.2.2. cancel()

申请取消任务。
将在下一个事件循环周期中将CancelledError异常抛给封装在Task中的协程。
收到CancelledError异常后,协程有机会处理异常,甚至以try …except CancelledError …finally来拒绝请求。因此,与Future.cancel()不同,Task.cancel()不能保证Task一定被取消掉。当然,拒绝取消请求这种操作并不常见,而且很不提倡。

2.2.3. cancelled()

如果 Task 已经被取消,则返回 True。
当取消请求通过 cancel() 被提交,且 Task 封装的协程传播了抛给它的 CancelledError 异常,则此 Task 被取消。

2.2.4. done()

如果 Task 已完成,则返回 True。
Task 完成有三种情况:

  • 封装的协程已返回
  • 封装的协程已抛出异常
  • Task 被取消

2.2.5. result()

返回 Task 的执行结果。
如果 Task 已经完成,则返回 Task 封装的协程的执行结果(如果 Task 封装的协程引发异常,则重新引发该异常)。
如果 Task 已经取消,则该方法引发 CancelledError 异常。
如果 Task 的结果还不可用,该方法引发 InvalidStateError 异常。

2.2.6. exception()

返回 Task 的异常。
如果封装的协程引发了异常,则返回此异常。如果封装的协程执行正常,则返回None。
如果 Task 已被取消,则引发 CancelledError 异常。
如果 Task 尚未完成,则引发 InvalidStateError 异常。

2.2.7. add_done_callback()

添加一个回调函数,在 Task 完成后执行。
这个方法只应用在基于回调的底层编程中。
具体细节可以参考 Future.remove_done_callback()

2.2.8 调试接口

get_stack(* ,limit=None)
print_stack(* ,limit=None,file=None)
classmethod all_tasks(loop=None)
calssmethod current_task(loop=None)
asyncio.iscoroutine(obj)
asyncio.iscoroutinefunction(func)

参考:https://blog.csdn.net/peng78585/article/details/128342847

2.3. Future(未来对象)

2.3.1. Future 创建

Future 是一种特殊的底层可等待对象,代表一个异步操作的最终结果。一般情况下,在应用层编程中,没有必要创建 Future 对象。
当一个 Future 对象被 await 的时候,表示当前的协程会持续等待,直到 Future 对象所指向的异步操作执行完毕。
在 asyncio 中,Future 对象能使基于回调的代码被用于 asyn/await 表达式中。
有时候,有些 Future 对象会被一些库和 asyncio API 暴露出来,我们可以 await 它们。

使用 asyncio.ensure_future(testa(1)) 返回一个 task 对象,此时 task 进入 pending 状态,并没有执行。这时 print(taska) 显示:>;print(taska.done()) 返回 False,表示它还没有结束。
当调用 await taska 时表示开始执行该协程,当执行结束以后,taska.done() 返回 True,这时可以调用taska.result() 得到函数的返回值。
如果协程还没有结束就调用 result() 方法则会抛个异常,raise InvalidStateError(‘Result is not ready.’)

3. 事件循环 Eventloop

3.1. 事件循环概述

Eventloop 是 asyncio 应用的核心,把一些异步函数注册到这个事件循环上,事件循环会循环执行这些函数。
当执行到某个函数时,如果它正在等待 I/O 返回(如它正在进行网络请求,或者 sleep 操作),事件循环会暂停它的执行去执行其他的函数;当某个函数完成 I/O 后会恢复,下次循环到它的时候继续执行。
因此,这些异步函数可以协同(Cooperative)运行:这就是事件循环的目标。
主线程和跑的协程函数是在同一个线程中。

  • 另一种表述

asyncio 就是遍历 event_loop 获取消息,是一个单线程阻塞函数。
在遍历循环过程中如果遇到某个协程阻塞就会卡住,一直等待结果返回,这时候就需要用到多线程防止阻塞。run_coroutine_threadsafe 函数在新线程中建立新event_loop,可以动态添加协程。

3.2. 事件循环执行原理

  1. 假设任务只有两个状态:
    预备状态: 是指任务目前空闲,但随时待命准备运行。
    等待状态: 是指任务已经运行,但正在等待外部的操作完成,比如 I/O 操作。
  2. 在这种情况下,Eventloop 会维护两个任务列表,分别对应这两种状态;并且选取预备状态的一个任务使其运行,一直到这个任务把控制权交还给 Eventloop 为止。
  3. 当任务把控制权交还给 Eventloop 时,Eventloop 会根据其是否完成,把任务放到预备或等待状态的列表中,然后遍历等待状态列表的任务,查看他们是否完成。
    如果完成,则将其放到预备状态的列表;
    如果未完成,则继续放在等待状态的列表。
  4. 而原先在预备状态列表的任务位置仍旧不变,因为它们还未运行。
  5. 当所有任务被重新放置在合适的列表后,新一轮的循环又开始了:Eventloop 继续从预备状态的列表中选取一个任务使其执行…如此周而复始,直到所有任务完成。

3.3. 事件循环执行协程

3.3.1. 协程函数执行(loop.run_until_complete)

我们修改以上的例程,并发执行两个协程。

import asyncio
import time

async def testa(x):
    print("in testa")
    await asyncio.sleep(x)
    print(f"Resuming a sleep {x}s")
    return x

async def testb(x):
    print("in testb")
    await asyncio.sleep(x)
    print(f"Resuming b sleep {x}s")
    return x

async def main():
    start_time = time.time()
    
    taska = asyncio.create_task(testa(3))
    taskb = asyncio.create_task(testb(1))

    result = await asyncio.gather(taska, taskb)
    print(result)

    end_time = time.time()
    print(f"use time:{end_time-start_time}")
    return "OK"

if __name__ == "__main__":
    loop = asyncio.get_event_loop()
    result = loop.run_until_complete(main())
    print(result)

运行结果:

in testa
in testb
Resuming b sleep 1s
Resuming a sleep 3s
[3, 1]
use time:3.0087671279907227
OK

总耗时 3s。跟阻塞最长的协程耗时一样。

3.3.2. 协程函数执行(loop.run_forever)

使用 loop.run_forever(),这个函数就像它的名字一样,会一直跑。只有事件循环跑起来了,那么使用该循环注册的协程才会得到执行。
如果使用 loop.run_forever() 则会阻塞在这里。
一般情况下,主线程和跑的协程函数是在同一个线程中。

import asyncio
import time
import threading
import os

async def testa(x):
    print(threading.current_thread(), "in testa", f"进程ID: {os.getpid()}")
    await asyncio.sleep(x)
    print(threading.current_thread(), f"Resuming a sleep {x}s")
    return x

async def testb(x):
    print(threading.current_thread(), "in testb", f"进程ID: {os.getpid()}")
    await asyncio.sleep(x)
    print(threading.current_thread(), f"Resuming b sleep {x}s")
    return x

# 定义一个跑事件循环的线程函数
def start_thread_loop(loop):
    asyncio.set_event_loop(loop=loop)
    loop.run_forever()

if __name__ == "__main__":
    print(threading.current_thread(), "In Main Thread", f"进程ID: {os.getpid()}")
    loop = asyncio.get_event_loop()
    # result = loop.run_until_complete(main())
    # 在子线程中运行事件循环

    t = threading.Thread(target=start_thread_loop, args=(loop, ))
    t.start()
    asyncio.run_coroutine_threadsafe(testa(3), loop=loop)
    asyncio.run_coroutine_threadsafe(testa(3), loop=loop)
    asyncio.run_coroutine_threadsafe(testb(1), loop=loop)
    asyncio.run_coroutine_threadsafe(testb(1), loop=loop)
    print("主线程不阻塞")

运行结果:

<_MainThread(MainThread, started 32532)> In Main Thread 进程ID: 26164
<Thread(Thread-1 (start_thread_loop), started 9804)> in testa 进程ID: 26164
<Thread(Thread-1 (start_thread_loop), started 9804)> in testa 进程ID: 26164
主线程不阻塞
# 以下内容为:在线程中执行,协程任务。
<Thread(Thread-1 (start_thread_loop), started 9804)> in testb 进程ID: 26164
<Thread(Thread-1 (start_thread_loop), started 9804)> in testb 进程ID: 26164
<Thread(Thread-1 (start_thread_loop), started 9804)> Resuming b sleep 1s
<Thread(Thread-1 (start_thread_loop), started 9804)> Resuming b sleep 1s
<Thread(Thread-1 (start_thread_loop), started 9804)> Resuming a sleep 3s
<Thread(Thread-1 (start_thread_loop), started 9804)> Resuming a sleep 3s

可以看到,主线程和协程任务所在线程的线程 ID 是同一个。
如何让主线程和子线程跑在了不同的线程中。解决方案是,先启一个子线程,这个线程用来跑事件循环 loop,然后动态的将同步函数添加到事件循环中。
事件循环还有一个 stop 属性,可以结束循环。

3.4. 动态添加同步函数

3.4.1. 动态添加同步函数(loop.call_soon_threadsafe)

import asyncio
import time
import threading

# 定义阻塞函数
def ping(url):
    print(threading.current_thread(), "in ping")
    time.sleep(2)
    print(threading.current_thread(), f"模拟 ping 输出 {url}")

# 定义一个跑事件循环的线程函数
def start_thread_loop(loop):
    asyncio.set_event_loop(loop=loop)
    loop.run_forever()

if __name__ == "__main__":
    print(threading.current_thread(), "In Main Thread")
    loop = asyncio.get_event_loop()
    # 在子线程中运行事件循环
    t = threading.Thread(target=start_thread_loop, args=(loop, ))
    t.start()

    # 在主线程中动态添加同步函数
    loop.call_soon_threadsafe(ping, "www.baidu.com")
    loop.call_soon_threadsafe(ping, "www.qq.com")
    loop.call_soon_threadsafe(ping, "www.weibo.com")
    print(threading.current_thread(), "主线程不会阻塞")

运行结果:

<_MainThread(MainThread, started 17992)> In Main Thread
<Thread(Thread-1 (start_thread_loop), started 30988)> in ping
<_MainThread(MainThread, started 17992)> 主线程不会阻塞
<Thread(Thread-1 (start_thread_loop), started 30988)> 模拟 ping 输出 www.baidu.com
<Thread(Thread-1 (start_thread_loop), started 30988)> in ping
<Thread(Thread-1 (start_thread_loop), started 30988)> 模拟 ping 输出 www.qq.com
<Thread(Thread-1 (start_thread_loop), started 30988)> in ping
<Thread(Thread-1 (start_thread_loop), started 30988)> 模拟 ping 输出 www.weibo.com

从输出结果可以看出,loop.call_soon_threadsafe() 和主线程不是跑在同一个线程中的。
虽然 loop.call_soon_threadsafe() 没有阻塞主线程的运行,但是由于需要跑的函数 ping 是阻塞式函数,所以调用了三次,这三次结果是顺序执行的,并没有实现并发。

3.4.2. 同步环境添加同步函数(loop.run_in_executor)

如果想要实现并发,需要通过 run_in_executor 把同步函数在一个执行器里去执行。
run_in_executor 的第一个参数是执行器,默认为空,这里执行器是使用 concurrent.futures 下的两个类,一个是 thread 一个是 process,也就是执行器可以分为线程执行器和进程执行器。它们在初始化的时候都有一个 max_workers 参数,如果不传则根据系统自身决定。

  • 默认执行器(线程执行器)
import asyncio
import time
import threading
import os

# 定义阻塞函数
def ping(url):
    print(threading.current_thread(), "in ping", f"进程ID: {os.getpid()}")
    time.sleep(2)
    print(threading.current_thread(), f"模拟 ping 输出 {url}")

# 定义一个跑事件循环的线程函数
def start_thread_loop(loop):
    asyncio.set_event_loop(loop=loop)
    loop.run_forever()

if __name__ == "__main__":
    print(threading.current_thread(), "In Main Thread", f"进程ID: {os.getpid()}")
    loop = asyncio.get_event_loop()
    # 在子线程中运行事件循环
    t = threading.Thread(target=start_thread_loop, args=(loop, ))
    t.start()

    # # 线程执行器,跑在了多线程
    # tx = concurrent.futures.ThreadPoolExecutor(max_workers=2)
    # # 进程执行器,跑在了多进程
    # px = concurrent.futures.ProcessPoolExecutor(max_workers=2)
    # # 在主线程中动态添加同步函数
    loop.run_in_executor(None, ping, "www.baidu.com")
    loop.run_in_executor(None, ping, "www.qq.com")
    loop.run_in_executor(None, ping, "www.weibo.com")
    print(threading.current_thread(), "主线程不会阻塞")

运行结果:

<_MainThread(MainThread, started 32240)> In Main Thread 进程ID: 15144
<Thread(asyncio_0, started 16448)> in ping 进程ID: 15144
<Thread(asyncio_1, started 30916)> in ping 进程ID: 15144
<Thread(asyncio_2, started 17784)> in ping 进程ID: 15144
<_MainThread(MainThread, started 32240)> 主线程不会阻塞
<Thread(asyncio_0, started 16448)> 模拟 ping 输出 www.baidu.com
<Thread(asyncio_2, started 17784)> 模拟 ping 输出 www.weibo.com
<Thread(asyncio_1, started 30916)> 模拟 ping 输出 www.qq.com

可以看到同步函数实现了并发,但是它们跑在了同一个进程不同的线程中,这个就和之前传统的使用多线程是一样的了。

  • 使用线程执行器
	......
    # 线程执行器,跑在了多线程
    tx = concurrent.futures.ThreadPoolExecutor(max_workers=2)
    # 进程执行器,跑在了多进程
    # px = concurrent.futures.ProcessPoolExecutor(max_workers=2)
    # 在主线程中动态添加同步函数
    loop.run_in_executor(tx, ping, "www.baidu.com")
    loop.run_in_executor(tx, ping, "www.qq.com")
    loop.run_in_executor(tx, ping, "www.weibo.com")
    print(threading.current_thread(), "主线程不会阻塞")

运行结果:

<_MainThread(MainThread, started 5428)> In Main Thread 进程ID: 26848
<Thread(ThreadPoolExecutor-0_0, started 37720)> in ping 进程ID: 26848
<Thread(ThreadPoolExecutor-0_1, started 7348)> in ping 进程ID: 26848
<_MainThread(MainThread, started 5428)> 主线程不会阻塞
<Thread(ThreadPoolExecutor-0_0, started 37720)> 模拟 ping 输出 www.baidu.com
<Thread(ThreadPoolExecutor-0_1, started 7348)> 模拟 ping 输出 www.qq.com
<Thread(ThreadPoolExecutor-0_1, started 7348)> in ping 进程ID: 26848
<Thread(ThreadPoolExecutor-0_1, started 7348)> 模拟 ping 输出 www.weibo.com

这们的进程 ID 相同,是跑在了同一个进程下。另外注意一下,我这里在初始化的时候传一个 max_workers 为 2,注意看结果的输出,它是先执行了前两个,当有一个执行完了以后再开始执行第三个,而不是三个同时运行的。

  • 使用进程执行器
	......
    # # 线程执行器,跑在了多线程
    # tx = concurrent.futures.ThreadPoolExecutor(max_workers=2)
    # 进程执行器,跑在了多进程
    px = concurrent.futures.ProcessPoolExecutor(max_workers=2)
    loop.run_in_executor(px, ping, "www.baidu.com")
    loop.run_in_executor(px, ping, "www.qq.com")
    loop.run_in_executor(px, ping, "www.weibo.com")
    print(threading.current_thread(), "主线程不会阻塞")

运行结果:

<_MainThread(MainThread, started 15444)> In Main Thread 进程ID: 20760
<_MainThread(MainThread, started 15444)> 主线程不会阻塞
<_MainThread(MainThread, started 8568)> in ping 进程ID: 23448
<_MainThread(MainThread, started 6560)> in ping 进程ID: 10596
<_MainThread(MainThread, started 6560)> 模拟 ping 输出 www.qq.com
<_MainThread(MainThread, started 8568)> 模拟 ping 输出 www.baidu.com
<_MainThread(MainThread, started 6560)> in ping 进程ID: 10596
<_MainThread(MainThread, started 6560)> 模拟 ping 输出 www.weibo.com

可以看出来它们的进程 ID 是不同的。

3.4.3. 异步环境添加异步函数(asyncio.run_coroutine_threadsafe)

https://blog.csdn.net/weixin_62077901/article/details/126402747
https://www.cnblogs.com/ydf0509/p/14216410.html

import asyncio
import concurrent.futures
import time
import threading
import concurrent
import os

async def testa(x):
    print(threading.current_thread(), "in testa", f"进程ID: {os.getpid()}")
    await asyncio.sleep(x)
    print(threading.current_thread(), f"Resuming a sleep {x}s")
    return x

async def testb(x):
    print(threading.current_thread(), "in testb", f"进程ID: {os.getpid()}")
    await asyncio.sleep(x)
    print(threading.current_thread(), f"Resuming b sleep {x}s")
    return x

# 定义一个跑事件循环的线程函数
def start_thread_loop(loop):
    asyncio.set_event_loop(loop=loop)
    loop.run_forever()

if __name__ == "__main__":
    print(threading.current_thread(), "In Main Thread", f"进程ID: {os.getpid()}")
    loop = asyncio.get_event_loop()
    # 在子线程中运行事件循环
    t = threading.Thread(target=start_thread_loop, args=(loop, ))
    t.start()

    asyncio.run_coroutine_threadsafe(testa(3), loop=loop)
    asyncio.run_coroutine_threadsafe(testa(3), loop=loop)
    asyncio.run_coroutine_threadsafe(testb(1), loop=loop)
    asyncio.run_coroutine_threadsafe(testb(1), loop=loop)
    print(threading.current_thread(), "主线程不会阻塞")

通过 asyncio.run_coroutine_threadsafe 在 loop 上绑定了四个协程函数,得到的输出结果为:

<_MainThread(MainThread, started 27320)> In Main Thread 进程ID: 27248
<Thread(Thread-1 (start_thread_loop), started 32148)> in testa 进程ID: 27248
<_MainThread(MainThread, started 27320)> 主线程不会阻塞
<Thread(Thread-1 (start_thread_loop), started 32148)> in testa 进程ID: 27248
<Thread(Thread-1 (start_thread_loop), started 32148)> in testb 进程ID: 27248
<Thread(Thread-1 (start_thread_loop), started 32148)> in testb 进程ID: 27248
<Thread(Thread-1 (start_thread_loop), started 32148)> Resuming b sleep 1s
<Thread(Thread-1 (start_thread_loop), started 32148)> Resuming b sleep 1s
<Thread(Thread-1 (start_thread_loop), started 32148)> Resuming a sleep 3s
<Thread(Thread-1 (start_thread_loop), started 32148)> Resuming a sleep 3s

主线程不会被阻塞,起的四个协程函数几乎同时运行,返回结果因为阻塞时长不一样而不同。但是注意,协程所在的线程和主线程不是同一个线程,因为此时事件循环 loop 是放到了另外的子线程中跑的,所以此时这四个协程放到事件循环的线程中运行的。
注意: 这里只有 run_coroutine_threadsafe 方法,没有 run_coroutine_thread 方法。
asyncio.run_coroutine_threadsafe() 和 run_in_executor() 返回的都是 Future 对象,所以可以将它们共同放到 gather 里,获取返回值。

3.4.4. run_coroutine_threadsafe 和 run_in_executor 的区别

参考:
https://www.cnblogs.com/ydf0509/p/14216410.html
https://www.cnblogs.com/ydf0509/p/14210083.html
含实例:
https://zhuanlan.zhihu.com/p/717068635

asyncio.run_coroutine_threadsafe 是在非异步上下文环境(被 def 函数里面)下调用异步函数对象(协程);
因为当前函数定义没有被 async 修饰,就不能在函数里面使用 await。这个是将 asyncio 包的 future 对象转化返回一个 concurrent.futures 包的 future 对象。

loop.run_in_executor 是在异步环境(被 async def 修饰的异步函数)里面,调用同步函数,将函数放到线程池运行,防止阻塞整个事件循环的其他任务。
这个是将 一个 concurrent.futures 包的 future 对象转化为 asyncio 包的 future 对象,
asyncio 包的 future 对象是一个 asyncio 包的 awaitable 对象,所以可以被 await,concurrent.futures.Future 对象不能被 await。

4. 实例

import asyncio
import concurrent.futures
import time
import threading
import concurrent
import os

async def testa(x):
    print(threading.current_thread(), "in testa", f"进程ID: {os.getpid()}")
    await asyncio.sleep(x)
    print(threading.current_thread(), f"Resuming a sleep {x}s")
    return x

async def testb(x):
    print(threading.current_thread(), "in testb", f"进程ID: {os.getpid()}")
    await asyncio.sleep(x)
    print(threading.current_thread(), f"Resuming b sleep {x}s")
    return x

# 定义一个跑事件循环的线程函数
def start_thread_loop(loop):
    asyncio.set_event_loop(loop=loop)
    loop.run_forever()

def shutdown(loop):
    print(threading.current_thread(), id(loop), "shutdown")
    # loop.stop # 不生效 loop.stop() 也不生效
    loop.call_soon_threadsafe(loop.stop)
    # t.join()

if __name__ == "__main__":
    print(threading.current_thread(), "In Main Thread", f"进程ID: {os.getpid()}")
    loop = asyncio.new_event_loop()

    # 在子线程中运行事件循环
    t = threading.Thread(target=start_thread_loop, args=(loop, ))
    t.start()
    asyncio.run_coroutine_threadsafe(testa(3), loop=loop)
    asyncio.run_coroutine_threadsafe(testb(1), loop=loop)

    # 等待 10s 之后,退出程序。期间 协程并发执行
    count = 0
    while count < 10:
        print("#####################", count, id(loop), loop.is_running())
        if loop.is_running():
            if count >= 5:
                shutdown(loop=loop)
        time.sleep(1)
        count += 1

运行结果:

<_MainThread(MainThread, started 22096)> In Main Thread 进程ID: 18612
<Thread(Thread-1 (start_thread_loop), started 14636)> in testa 进程ID: 18612
<Thread(Thread-1 (start_thread_loop), started 14636)> in testb 进程ID: 18612
##################### 0 2523187789776 True
<Thread(Thread-1 (start_thread_loop), started 14636)> Resuming b sleep 1s
##################### 1 2523187789776 True
##################### 2 2523187789776 True
<Thread(Thread-1 (start_thread_loop), started 14636)> Resuming a sleep 3s
##################### 3 2523187789776 True
##################### 4 2523187789776 True
##################### 5 2523187789776 True
<_MainThread(MainThread, started 22096)> 2523187789776 shutdown
##################### 6 2523187789776 False
##################### 7 2523187789776 False
##################### 8 2523187789776 False
##################### 9 2523187789776 False

5. 原语列表

官网地址:
https://docs.python.org/3/library/asyncio-future.html#asyncio.Future.remove_done_callback

5.1. asyncio 原语

5.1.1. asyncio.run(执行程序)

# 如果 debug=True,事件循环将运行在 调试模式。
asyncio.run(coro, * , debug=False)

这个函数运行 coro 参数指定的协程,负责管理 asyncio 事件循环,终止异步生成器。
在同一个线程中,当已经有 asyncio 事件循环在执行时,不能调用此函数。
此函数总是创建一个新的事件循环,并在最后关闭它。建议将它用作 asyncio 程序的主入口,并且只调用一次。
重要: 这个函数是在 Python3.7 被临时添加到 asyncio 中的。

5.1.2. asyncio.create_task(创建对象)

asyncio.create_task(coro)

将 coro 参数指定的协程(coroutine)封装到一个 Task 中,并调度执行。返回值是一个 Task 对象。
任务在由 get_running_loop() 返回的事件循环(loop)中执行。如果当前线程中没有正在运行的事件循环,将会引发 RuntimeError 异常:

Traceback (most recent call last):
  File "D:\works\demo\multi\modelmulti\test.py", line 33, in <module>
    task = asyncio.create_task(main())
  File "D:\programs\anaconda3\envs\python310\lib\asyncio\tasks.py", line 336, in create_task
    loop = events.get_running_loop()
RuntimeError: no running event loop
sys:1: RuntimeWarning: coroutine 'main' was never awaited

5.1.3. asyncio.ensure_future

asyncio.ensure_future(coro)

asyncio.create_task 已经被引入到 Python3.7。在 Python 早期版本中,可以使用底层函数 asyncio.ensure_future() 代替。

5.1.4. asyncio.sleep(Sleeping)

# delay 阻塞秒数 
# 如果指定了 result 参数的值,则在协程结束时,将该值返回给调用者。
coroutine asyncio.sleep(delay, result=None, * , loop=None)

sleep() 通常只暂停当前 task,并不影响其他 task 的执行。
不建议使用 loop 参数,因为 Python 计划在3.10 版本中移除它。

5.1.5. asyncio.gather(协程并发执行)

协程并发执行需要将协程放到 asyncio.gather() 中运行,以下是该函数原型:

# aws 参数指定可等待(awaitable)对象序列。
# 如果 return_execptions 参数为 False(默认值即为 False),引发的第一个异常会立即传播给等待 gather() 的任务,即调用 await asyncio.gather() 对象。序列中其他 awaitable对象的执行不会受影响。
# 如果 return_exceptions 参数为 True,异常会和正常结果一样,被聚合到结果列表中返回。
awaitable asyncio.gather(* aws, loop=None, return_exceptions=False)

如果 aws 序列中的某个 awaitable 对象是一个协程,则自动将这个协程封装为 Task 对象进行处理。
如果所有的 awaitable 对象都执行完毕,则返回 awaitable 对象执行结果的聚合列表。返回值的顺序于 aws 参数的顺序一致。
如果 gather() 被取消,则提交的所有 awaitable 对象(尚未执行完成的)都会被取消。
如果 aws 中某些 Task 或 Future 被取消,gather() 调用不会被取消,被取消的 Task 或 Future 会以引发 CancelledError 的方式被处理。这样可以避免个别 awaitable 对象的取消操作影响其他 awaitable 对象的执行。

......
async def main():
    start_time = time.time()
    resulta, resultb = await asyncio.gather(testa(3), testb(1))
    print(f"test resulta is {resulta}")
    print(f"test resultb is {resultb}")
    end_time = time.time()
    print(f"use time:{end_time-start_time}")
......

运行结果:

in testa
in testb
Resuming b sleep 1s
Resuming a sleep 3s
test resulta is 3
test resultb is 1
use time:3.0111238956451416

可以看到,testa 和 testb 是同步在运行,由于 testb 只 sleep 了1秒钟,所以 testb 先输出,最后将每个协程函数的结果返回。运行总时长3秒。
注意: 这里是 gather() 函数里的每一个协程函数都执行完了,它才结果,结果是一个列表,列表里的值顺序和放到 gather 函数里的协程的顺序是一致的。

5.1.6. asyncio.shield(避免取消)

awaitable asyncio.shield(aw, * , loop=None)

防止 awaitable 对象被取消(cancelled)执行。
如果 aw 参数是一个协程(coroutines),该对象会被自动封装为 Task 对象进行处理。

# code 1
res = await shield(something())
# code 2
res = await something()

通常情况下,code 1 和 code 2 是等价的。
特殊情况是,如果包含以上代码的协程(父协程)被取消,code 1 与 code 2 的执行效果就完全不同了:
code 1 中,运行于 something() 中的任务不会被取消
code 2 中,运行于 something() 中的任务会被取消
在 code 1 中,从 something() 的视角看,取消操作并没有发生。然而,事实上它的调用者确实被取消了,所以 await shield(something()) 仍然会引发一个 CancelledError 异常。
如果 something() 以其他的方式被取消,比如从自身内部取消,那么 shield() 也会被取消。
如果希望完全忽略取消操作(不推荐这么做),则可以将 shield() 与 try/except 结合起来使用:

try:
    res = await shield(something())
except CancelledError:
    res = None

5.1.7. asyncio.wait_for(超时 Timeouts)

# timeout 可以是 None 也可以是一个 float 或 int 类型的数字,表示需要等待的秒数。
# 如果 timeout 是 None,则永不超时,一直阻塞到 aw 执行完毕。
coroutine asyncio.wait_for(aw, timeout, * , loop=None)

在 timeout 时间之内,等待 aw 参数指定的 awaitable 对象执行完毕。
如果达到 timeout 时间,将会取消待执行的任务,引发 asyncio.TimeoutError。
如果想避免任务被取消,可以将其封装在 shield() 中。
程序会等待到任务确实被取消掉,所以等待的总时间会比 timeout 略大。
如果 await_for() 被取消,aw 也会被取消。
loop 参数将在 Python3.10 中删除,所以不推荐使用。
Python3.7 新特性:当 aw 因为超时被取消,wait_for() 等到 aw 确实被取消之后返回异常。在以前的版本中,wait_for 会立即返回异常。

5.1.8. asyncio.wait(等待原语)

# timeout 参数可以是一个 int 或 float 类型的值,可以控制最大等待时间。
# return_when 决定函数返回的时机。
# FIRST_COMPLETED(第一个结束时返回)	The function will return when any future finishes or is cancelled.
# FIRST_EXCEPTION(第一个出现异常)	The function will return when any future finishes by raising an exception. If no future raises an exception then it is equivalent to ALL_COMPLETED.
# ALL_COMPLETED(全部执行完,默认的)	The function will return when all futures finish or are cancelled.
coroutine asyncio.wait(aws, * , loop=None, timeout=None, return_when=ALL_COMPLETED)

并发执行 aws 中的 awaitable 对象,一直阻塞到 return_when 指定的情况出现。
如果 aws 中的某些对象是协程(coroutine),则自动转换为 Task 对象进行处理。直接将 coroutine 对象传递给 wait() 会导致令人迷惑的执行结果,所以不建议这么做。
返回值是个 Task/Future 集合:(done, pending)。

done,pending = await asyncio.wait(aws)

需要注意的是,wait() 不会引发 asyncio.TimeoutError 错误。返回前没有被执行的 Future 和 Task 会被简单的放入 pending 集合。
与 wait_for() 不同,wait() 不会再超时的时候取消任务。

https://blog.csdn.net/peng78585/article/details/128342847

5.1.9. asyncio.as_completed

asyncio.as_completed(aws, * , loop=None, timeout=None)

并发执行 aws 中的 awaitable 对象。返回一个 Future 对象迭代器。每次迭代时返回的Future 对象代表待执行的 awaitable 对象集合里最早出现的结果。
注意:迭代器返回的顺序与 aws 列表的顺序无关,只与结果出现的早晚有关。
如果超时之前还有 Future 对象未完成,则引发 asyncio.TimeoutError 异常。

5.1.10. asyncio.run_coroutine_threadsafe

asyncio.run_coroutine_threadsafe(coro,loop)

向 loop 指定的事件循环提交一个由 coro 指定协程。线程安全。
返回一个 concurrent.futures.Future 对象,等待另一个线程返回结果。
这个函数用于从当前线程向运行事件循环的线程提交协程(coroutine)。
如果协程出现异常,返回的 Future 会收到通知。返回的 Future 也可以被用作取消事件循环中的任务。
与其他 asyncio 函数不同,该函数需要显式传递 loop 参数

5.1.11. asyncio.current_task

asyncio.current_task(loop=None)

返回事件循环中正在运行的 Task 实例,如果没有 Task 在执行,则返回 None。
如果 loop 为 None,则使用 get_running_loop() 获取当前事件循环。
新增于 Python3.7

5.1.12. asyncio.all_tasks

asyncio.all_tasks(loop=None)

返回事件循环中尚未运行结束的 Task 对象集合。
如果 loop 为 None,则,使用 get_running_loop() 获取当前事件循环。
新增于 Python3.7

5.2. loop 原语

5.2.1. loop.run_until_complete(协程函数执行)

想要使得协程函数得到执行,需要调用事件循环来执行任务,loop.run_until_complete 就是使循环开始执行了。
loop.run_until_complete 是一个阻塞方法,只有当它里面的协程运行结束以后这个方法才结束,才会运行之后的代码。
其实也可以不调用 loop.run_until_complete 方法,创建一个 task 以后,其实就已经在跑协程函数了,只不过当事件循环如果准备开始运行了,此时的 task 状态是 pending,如果不调用事件循环的话,则不会运行协程函数,由于主线程跑完了,子线程也就被销毁了。
loop.run_until_complete 的参数是协程对象或者 futures,也可以传 Task 对象,因为 Task 是 futures 的子类,当传入的是一个协程对象时,返回一个 Task 对象,传入一个 futures 的时候,直接返回 futures 对象,也就是说,在调用 asyncio.ensure_future() 以后,都会返回一个 Task 对象,都可以为它添加一个回调方法,并且可以调用 task.result() 方法得到结果(注意如果 task 没有执行结束就调用 result 方法,则会抛异常)。

5.2.2. loop.run_forever(协程函数执行)

loop.run_forever(),这个函数就像它的名字一样,会一直跑。只有事件循环跑起来了,那么使用该循环注册的协程才会得到执行,但是如果使用 loop.run_forever() 则会阻塞在这里,事件循环还有一个 stop 方法,可以结束循环,我们可以在 task 对象上添加一个回调方法,当协程执行结束以后,调用事件循环的 stop 方法来结束整个循环。

5.2.3. loop.stop

5.2.4. loop.call_soon_threadsafe(动态添加同步函数)

5.2.5. loop.run_in_executor(动态添加同步函数)

该方法需要传入三个参数,
# executor 执行器,默认可以传入 None,如果传入的是 None,将使用默认的执行器,一般执行器可以使用线程或者进程执行器。
# 这里执行器是使用 concurrent.futures 下的两个类,一个是 thread 一个是 process
# func 协程
# args 协程参数
run_in_executor(executor, func, *args)

你可能感兴趣的:(async,python,python,开发语言)