深入Asyncio(十一)优雅地开始与结束
Startup and Shutdown Graceful
大部分基于asyncio的程序都是需要长期运行、基于网络的应用,处理这种应用的正确开启与关闭存在惊人的复杂性。
开启相对来说更简单点,常规做法是创建一个task,然后调用loop.run_forever(),就如第三章QuickStart中的例子一样。
一个例外是当启动监听服务器时需要经过两个阶段:
- 为服务器的启动创建一个coroutine,然后调用
run_until_complete()
来初始化并启动服务器本身; - 通过调用
loop.run_forever()
来调用main函数。
通常启动是很简单的,碰到上述例外情况,查看官方示例。
关闭就要复杂得多,之前讲过run_forever()
调用会阻塞主线程,当执行关闭时,会解除阻塞并执行后续代码,此时就需要:
- 收集所有尚未完成的task对象;
- 将他们聚集到一个group任务中;
- 取消group任务(需要捕捉CancelledError);
- 通过
run_until_complete()
来等待执行完毕。
在这之后关闭才算完成,初学者在写异步代码时总是极力摆脱的一些错误信息比如task还未等待就被关闭了,主要原因就是遗失了上述步骤中的一个或多个,用个例子来说明。
import asyncio
async def f(delay):
await asyncio.sleep(delay)
loop = asyncio.get_event_loop()
t1 = loop.create_task(f(1)) # 任务1执行1秒
t2 = loop.create_task(f(2)) # 任务2执行2秒
loop.run_until_complete(t1) # 只有任务1被执行完成
loop.close()
λ python3 taskwaring.py
Task was destroyed but it is pending!
task: <Task pending coro=<f() running at taskwaring.py:4> wait_for=<Future pending cb=[<TaskWakeupMethWrapper object at 0x0312D6D0>()]>>
这个错误是说有些任务在loop关闭时还没完成,这也就是为什么规范的关闭过程要将所有的task收集到一个task中,取消它们然后在loop关闭之前等待取消完成。
再多看些比QuickStart代码更细节的例子,这次用官方文档中的echo服务器代码作为服务器,通过客户端代码来深入学习。
from asyncio import (
get_event_loop,
start_server,
CancelledError,
StreamReader,
StreamWriter,
Task,
gather
)
async def echo(reader: StreamReader, writer: StreamWriter): # 1
print('New connection.')
try:
while True: # 2
data: bytes = await reader.readlines() # 3
if data in [b'', b'quit']:
break
writer.write(data.upper()) # 4
await writer.drain()
print('Leaving Connection.')
except CancelledError: # 5
writer.write_eof()
print('Cancelled')
finally:
writer.close()
loop = get_event_loop()
coro = start_server(echo, '127.0.0.1', 8888, loop=loop) # 6
server = loop.run_until_complete(coro) # 7
try:
loop.run_forever() # 8
except KeyboardInterrupt:
print('Shutting Down!')
server.close() # 9
loop.run_until_complete(server.wait_closed()) # 10
tasks = Task.all_tasks() # 11
group = gather(*tasks, return_exceptions=True) # 12
group.cancel()
loop.run_until_complete(group) # 13
loop.close()
这个协程用于为每个建立的连接创建一个协程,使用了Stream的API;
为了保持连接,用死循环获取消息;
从服务器获取信息;
将消息的字符全部大写返回;
此处处理退出,进行环境退出的清理工作;
这里是程序开始的地方,服务器需要单独循行,start_server方法返回一个corountine,必须在run_until_complete中执行;
运行coroutine来启动TCP服务器;
现在才开始程序的监听部分,为连接到服务器的每个TCP生成一个coroutine来执行echo例程函数,唯一能打断loop的只能是KeyboardInterrupt异常;
程序运行到这里的话,关闭操作已经开始,从现在开始要让服务器停止接受新的连接,第一步是调用server.close();
第二步是调用server.wait_closed()来关闭那些仍在等待连接建立的socket,仍处于活跃状态的连接不会受影响;
开始关闭task,先收集当前所有等待状态的task;
将task聚集到一个group中,然后调用cancel方法,此处的return_exceptions参数后面讲;
运行group这个协程。
要注意的一点是,如果在一个coroutine内部捕捉了一个CancelledError,要注意在异常捕捉代码中不要创建任何coroutine,all_tasks()
无法感知在run_until_complete()
运行阶段创建的任何新任务。
return_exceptions=True
参数是干什么的?
gather()
方法有个默认参数是return_exceptions=False,通过默认设置来关闭异常处理是有问题的,很难直接解释清楚,可以通过一系列事实来说明:
1. run_until_complete()
方法执行Future对象,在关闭期间,执行由gather()
方法返回的Future对象;
2. 如果这个Future对象抛出了一个异常,那么这个异常会继续向上抛出,导致loop停止;
3. 如果run_until_complete()
被用来执行一个group Future对象,任何group内子任务未处理而抛出的异常都会被向上抛出,也包含CancelledError;
4. 如果一部分子任务处理了CancelledError异常,另一部分未处理,则未处理的那部分的异常也会导致loop停止,这意味着loop在所有tasks完成前就停止了;
5. 在关闭loop时,不希望上述特性被触发,只是想要所有在group中的task尽快执行结束,也不理会某些task是否抛出异常;
6. 使用gather(*, return_exceptions=True)
可以让group将子任务中的异常当作返回值处理,因此不会影响run_until_complete()
的执行。
关于捕获异常不合人意的一点就是某些异常在group内被处理了而没有被抛出,这对通过结果查找异常、写logging造成了困难。
import asyncio
async def f(delay):
await asyncio.sleep(1/delay) # 传入值是0就很恶心了
return delay
loop = asyncio.get_event_loop()
for i in range(10):
loop.create_task(f(i))
pending = asyncio.Task.all_tasks()
group = asyncio.gather(*pending, return_exceptions=True)
results = loop.run_until_complete(group)
print(f'Results: {results}')
loop.close()
不设置参数的话就会导致异常被向上抛出,然后loop停止并导致其他task无法完成。安全退出是网络编程最难的问题之一,这对asyncio也是一样的。
Signals
在上一个例子中演示了如何通过KeyboardInterrupt
来退出loop,这个异常有效地结束了run_forever()
的阻塞,并允许后续代码得以执行。
KeyboardInterrupt
异常等同于SIGINT
信号,在网络服务中最常用的停止信号其实是SIGTERM
,并且也是在UNIX shell环境中使用kill
指令发出的默认信号。
在UNIX系统中kill
指令其实就是发送信号给进程,不加参数地调用就会发送TERM
信号使进程安全退出或被忽视掉,通常这不是个好办法,因为如果进程没有退出,kill
就会发送KILL信号来强制退出,这会导致你的程序无法可控地结束。
asyncio原生支持处理进程信号,但处理一般信号的复杂度太高(不是针对asyncio),本文不会深入讲解,只会挑一些常见信号来举例。先看下例:
# shell_signal01.py
import asyncio
async def main(): # 这里是应用的主体部分,简单的用一个死循环来表示程序运行
while True:
print('<Your app is running>')
await asyncio.sleep(1)
if __name__ == "__main__":
loop = asyncio.get_event_loop()
loop.create_task(main()) # 这里与前几个例子一样,将coroutine添加到loop中
try:
loop.run_forever()
except KeyboardInterrupt: # 在本例中,只有Ctrl-C会终止loop,然后像前例中进行善后工作
print('<Got signal: SIGINT, shutting down.>')
tasks = asyncio.Task.all_tasks()
group = asyncio.gather(*tasks, return_exceptions=True)
group.cancel()
loop.run_until_complete(group)
loop.close()
这些很简单,下面思考一些复杂的功能:
1. 产品需要将SIGINT和SIGTERM都当作停止信号;
2. 需要在应用的main()
中处理CancelledError
,并且处理异常的代码也需要一小段时间来运行(例如有一堆网络连接需要关闭);
3. 应用多次接收停止信号不会出现异常,在接收到一次停止信号后,后续的信号都不作处理。
asyncio提供了足够粒度的API来处理这些场景。
# shell_signal02.py
import asyncio
from signal import SIGINT, SIGTERM # 从标准库中导入信号值
async def main():
try:
while True:
print('<Your app is running>')
await asyncio.sleep(1)
except asyncio.CancelledError: # 1
for i in range(3):
print('<Your app is shtting down...>')
await asyncio.sleep(1)
def handler(sig): # 2
loop.stop() # 3
print(f'Got signal: {sig}, shtting down.')
loop.remove_signal_handler(SIGTERM) # 4
loop.add_signal_handler(SIGINT, lambda: None) # 5
if __name__ == "__main__":
loop = asyncio.get_event_loop()
for sig in (SIGINT, SIGTERM): # 6
loop.add_signal_handler(sig, handler, sig)
loop.create_task(main())
loop.run_forever()
tasks = asyncio.Task.all_tasks()
group = asyncio.gather(*tasks, return_exceptions=True)
group.cancel()
loop.run_until_complete(group)
loop.close()
现在在coroutine内部处理停止业务,在调用group.cancel()时收到取消信号,在处理关闭loop的run_until_complete阶段,main将继续运行一段时间;
这是收到信号后的回调函数,它通过add_signal_handler()修改了loop的配置;
在回调函数开始执行时,首先要停止loop,这使得关闭业务代码开始执行;
此时已经开始停止代码业务,因此移除SIGTERM来忽视后续的停止信号,否则会使停止代码业务也被终止;
原理与上面类似,但SIGINT不能简单地remove,因为KeyboardInterrupt默认是SIGINT信号的handler,需要将SIGINT的handler置空;
在这里配置信号的回调函数,都指向handler,因此配置了SIGINT的handler,会覆盖掉默认的KeyboardInterrupt。
在关闭过程中等待Executor执行
在QuickStart中有一段代码使用了阻塞的sleep()
调用,当时说明了一个情况即如果该阻塞调用耗时比loop的执行耗时长时会发生什么,现在来讨论,先放结论,如果不进行人工干预将会得到一系列errors。
import time
import asyncio
async def main():
print(f'{time.ctime()} Hello!')
await asyncio.sleep(1.0)
print(f'{time.ctime()} Goodbye!')
loop.stop()
def blocking():
time.sleep(1.5)
print(f"{time.ctime()} Hello from a thread!")
loop = asyncio.get_event_loop()
loop.create_task(main())
loop.run_in_executor(None, blocking)
loop.run_forever()
tasks = asyncio.Task.all_tasks(loop=loop)
group = asyncio.gather(*tasks, return_exceptions=True)
loop.run_until_complete(group)
loop.close()
λ python3 quickstart.py
Sun Sep 30 14:11:57 2018 Hello!
Sun Sep 30 14:11:58 2018 Goodbye!
Sun Sep 30 14:11:59 2018 Hello from a thread!
exception calling callback for <Future at 0x36cff70 state=finished returned NoneType>
Traceback (most recent call last):
...
raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed
来看下背后发生了什么,run_in_executor()
返回的是Future而不是Task,这说明它不能被asyncio.Task.all_tasks()
感知,所以后续的run_until_complete()
也就不会等待这个Future执行完毕。
有三个解决思路,都经过了不同程度的权衡,下面逐个过一遍,从不同视角观察事件loop的内涵,思考在程序中相互调用的所有coroutine、线程、子进程的生命周期管理。
第一个思路,将executor放到coroutine中并以此建立一个task。
# OPTION-A
import time
import asyncio
async def main():
print(f'{time.ctime()} Hello!')
await asyncio.sleep(1.0)
print(f'{time.ctime()} Goodbye!')
loop.stop()
def blocking():
time.sleep(2.0)
print(f"{time.ctime()} Hello from a thread!")
async def run_blocking(): # 1
await loop.run_in_executor(None, blocking)
loop = asyncio.get_event_loop()
loop.create_task(main())
loop.create_task(run_blocking()) # 2
loop.run_forever()
tasks = asyncio.Task.all_tasks(loop=loop)
group = asyncio.gather(*tasks, return_exceptions=False)
loop.run_until_complete(group)
loop.close()
这个想法是run_in_executor返回的Future而不是task,虽然无法用all_tasks()捕获,但可以用await等待一个Future,所以用一个新的coroutine来await在executor中的阻塞调用,这个新的coroutine将被作为task添加到loop;
就像运行main一样将这个coroutine添加到loop中。
上述代码看起来不错,除了不能执行任务取消。可以发现代码中少了group.cancel()
,倘若加回来又会得到Event loop is closed
错误,甚至不能在run_blocking()
中处理CancelledError以便重新await Future,无论做什么该task都会被取消,但executor会将其内部的sleep执行完。
第二个思路,收集尚未完成的task,仅取消它们,但在调用run_until_complete()
之前要将run_in_executor()
生成的Future添加进去。
# OPTION-B
import time
import asyncio
async def main():
print(f'{time.ctime()} Hello!')
await asyncio.sleep(1.0)
print(f'{time.ctime()} Goodbye!')
loop.stop()
def blocking():
time.sleep(2.0)
print(f"{time.ctime()} Hello from a thread!")
loop = asyncio.get_event_loop()
loop.create_task(main())
future = loop.run_in_executor(None, blocking) # 1
loop.run_forever()
tasks = asyncio.Task.all_tasks(loop=loop) # 2
group_tasks = asyncio.gather(*tasks, return_exceptions=True)
group_tasks.cancel() # 取消tasks
group = asyncio.gather(group_task, future) # 3
loop.run_until_complete(group)
loop.close()
记录返回的Future;
此处loop已停止,先获得所有task,注意这里面没有executor的Future;
创建了一个新的group来合并tasks和Future,在这种情况下executor也能正常退出,而tasks仍然通过正常的cancel来取消。
这个解决办法在关闭时比较友好,但仍然有缺陷。通常来说,在整个程序中通过某种方式收集所有的executor返回的Future对象,然后与tasks合并,然后等待执行完成,这十分不方便,虽然有效,但还有更好的解决办法。
# OPTION-C
import time
import asyncio
from concurrent.futures import ThreadPoolExecutor as Executor
async def main():
print(f'{time.ctime()} Hello!')
await asyncio.sleep(1.0)
print(f'{time.ctime()} Goodbye!')
loop.stop()
def blocking():
time.sleep(2.0)
print(f"{time.ctime()} Hello from a thread!")
loop = asyncio.get_event_loop()
executor = Executor() # 1
loop.set_default_executor(executor) # 2
loop.create_task(main())
future = loop.run_in_executor(None, blocking) # 3
loop.run_forever()
tasks = asyncio.Task.all_tasks(loop=loop)
group = asyncio.gather(*tasks, return_exceptions=True)
group.cancel()
loop.run_until_complete(group)
executor.shutdown(wait=True) # 4
loop.close()
建立自己的executor实例;
将其设定为loop的默认executor;
像以前一样;
明确地在loop关闭前等待executor的所有Future执行完,这可以避免"Event loop is closed"这样的错误信息,能这样做是因为获得了使用executor的权限,而asyncio默认的executor没有开放相应的接口调用。
现在可以在任何地方调用run_in_executor()
,并且程序可以优雅地退出了。
深入Asyncio(十一)优雅地开始与结束的更多相关文章
- python 携程asyncio 实现高并发示例2
https://www.bilibili.com/video/BV1g7411k7MD?from=search&seid=13649975876676293013 import asyncio ...
- asyncio异步编程【含视频教程】
不知道你是否发现,身边聊异步的人越来越多了,比如:FastAPI.Tornado.Sanic.Django 3.aiohttp等. 听说异步如何如何牛逼?性能如何吊炸天....但他到底是咋回事呢? 本 ...
- 【积累】如何优雅关闭SpringBoot Web服务进程
1.使用ps ef查出进程对应的pid. 2.使用kill -15 pid结束进程. 为什么不使用kill -9 pid,个人理解kill -15 pid更优雅,能在结束进程前执行spring容器清理 ...
- asyncio异步模块的21个协程编写实例
启动一个无返回值协程 通过async关键字定义一个协程 import sys import asyncio async def coroutine(): print('运行协程') if sys.ve ...
- asyncio异步编程
1. 协程 协程不是计算机提供,程序员认为创造 协程(Coroutine),也可以被称为微线程,是一种用户态内的上下文切换技术,其实就是一个线程实现代码块相互切换执行.例如: def func1(): ...
- IOS开发-表单控件的应用
1. 需求描述 2. 开发环境介绍 3. 创建一个工程 4. 工程配置介绍 5. 目录结构介绍 6. 界面设置 7. 关联输入输出 8. 关联事件代码 9. 运行结果 10. UITextField ...
- 【转】WF4.0 (基础篇)
转自:http://www.cnblogs.com/foundation/category/215023.html 作者:WXWinter —— 兰竹菊梅★春夏秋冬☆ —— wxwinter@16 ...
- workflow4.0学习资料
http://www.cnblogs.com/foundation/archive/2010/04/03/1703809.html 2篇说明: WF4 Bata 2 WF4.0 RC 对比 Beta2 ...
- Golang 微框架 Gin 简介
框架一直是敏捷开发中的利器,能让开发者很快的上手并做出应用,甚至有的时候,脱离了框架,一些开发者都不会写程序了.成长总不会一蹴而就,从写出程序获取成就感,再到精通框架,快速构造应用,当这些方面都得心应 ...
随机推荐
- 倒计时浏览器跳转JavaScript
原文发布时间为:2008-10-11 -- 来源于本人的百度文章 [由搬家工具导入] <html> <head> <title>显示时间</title> ...
- 《Linux命令行与shell脚本编程大全 第3版》Linux命令行---53
以下为阅读<Linux命令行与shell脚本编程大全 第3版>的读书笔记,为了方便记录,特地与书的内容保持同步,特意做成一节一次随笔,特记录如下:
- SaltStack 模块学习之拷贝master服务器上文件和目录到minion服务器
一. cp.get_file实现从master端复制文件到minion服务器的文件中cp.get_file 1. 修改/etc/salt/master ,指定server 工作的根目录 file- ...
- 用C#将XML转换成JSON
本文旨在介绍如果通过C#将获取到的XML文档转换成对应的JSON格式字符串,然后将其输出到页面前端,以供JavaScript代码解析使用.或许你可以直接利用JavaScript代码通过Ajax的方式来 ...
- JS-JavaScript String 对象-string对象方法2: indexOf()、lastIndexOf()、charAt()
1.indexOf():可返回某个指定的字符串值在字符串中首次出现的位置. 1).语法:string.indexOf(searchvalue,start): searchvalue:必需.规定 ...
- 关于超大binlog事件的问题
我手里维护了一个项目,其功能是用Java模拟一个MariaDB的slave库连接到主库,对从主库传输过来的binlog事件进行监听与分析 碰到一个问题是: 如果主库做了一个很大的修改操作(比方说直接d ...
- json.net(Json.NET - Newtonsoft)利用动态类解析json字符串
将对象转换为字符串很简单,唯一要注意的点就是为了避免循环要在需要的字段上添加jsonignore属性.可以参照这篇博文:http://www.mamicode.com/info-detail-1456 ...
- 可靠UDP设计
最近加入了一个用帧同步的项目,帧同步方案对网络有着极大的影响,于是采用了RUDP(可靠UDP),那么为什么要摒弃TCP,而费尽心思去采用UDP呢?要搞明白这个问题,首先要了解TCP和UDP的区别 , ...
- asp.net上传文件夹权限配置以及权限配置的分析
切记:一定要禁止给公共上传文件夹的权限设置为everyone,且为完全控制!除非你这个文件夹属于内部操作的,那这样做是允许,其余情况一律禁止! 基本的文件上传文件夹权限配置: 1.在需要配置上传的文件 ...
- ARM Linux系统调用的原理
转载自:http://blog.csdn.net/hongjiujing/article/details/6831192 ARM Linux系统调用的原理 操作系统为在用户态运行的进程与硬件设备进行交 ...