Python asyncio的一个坑

我们先从一个常见的Python编程错误开始说起,我已经见过非常多的程序员犯过这种错误了:

def do_not_raise(user_defined_logic):
    try:
        user_defined_logic()
    except:
        logger.warning("User defined logic raises an exception", exc_info=True)
        # ignore

这段代码的错误之处在哪里呢?

我们从Python的异常结构开始说起。Python中的异常基类有两个,最基础的是BaseException,第二个是Exception(继承BaseException)。这两者有什么区别呢?

Exception代表大部分我们经常会在业务逻辑中处理到的异常,也包括一部分运行出错例如NameErrorAttributeError等等。但是并不是所有的异常都是Exception类的子类,少数几个异常是继承于BaseException的:

  • GeneratorExit
  • SystemExit
  • KeyboardInterrupt

第一个代表生成器被close()方法关闭,第二个代表系统退出(例如使用sys.exit),第三个代表程序被Ctrl+C中断。之所以它们并不继承于Exception,是因为:它们一般情况下绝不应当被捕获,或者被捕获之后应当立即reraise(通过不带参数的raise语句)。

如果写出上面那样的语句,就可能会出现程序无法退出的情况:从外部发送SIGTERM信号到程序,触发了SystemExit,然而SystemExit被捕获然后忽略了,这样程序就没有正常退出,而是继续执行下去。像SystemExit、KeyboardInterrupt、GeneratorExit这样的异常,因为没有固定的抛出位置,所以如果乱捕获的话非常危险,很可能产生隐含的bug,而且测试中会很难发现。这就是为什么Python官方文档上会强调,如果使用无参数的except,一定要配合raise重新将异常抛出。而正确的忽略执行异常的方法应该是:

def do_not_raise(user_defined_logic):
   try:
       user_defined_logic()
   except Exception:          ### <= Notice here ###
       logger.warning("User defined logic raises an exception", exc_info=True)
       # ignore

那么说了这么多,跟asyncio有什么联系呢?

asyncio当中,一个异步过程可以通过asyncio.Task作为一个独立执行的单元启动,这个Task对象有一个cancel()方法,可以将它从中途强制停止。类似的,异步生成器也可以通过aclose()方法强制结束。当一个异步过程或者异步生成器被从外部强制中止的时候,会从当前的await或者yield语句抛出asyncio.CancelledError

问题就出在这个CancelledError上!

asyncio也许是为了偷懒,也许是为了和concurrent一致,这个异常实际上是concurrent.futures.CancelledError。它的基类是Exception,而不是BaseException。要知道,在concurrent库当中,CancelledError是不会抛到已经开始了的子过程中的,它只会从future对象里抛出;而asyncio中,当使用了cancel()方法的时候,这个异常会从Task的当前堆栈位置抛出来。

这个事情就尴尬了,如果前面的do_not_raise是个异步方法,用 except Exception来捕获了用户自定义方法中的异常,那CancelledError也会被捕获到。结果就是CancelledError被错误地忽略掉,导致cancel()方法没有成功终止掉一个Task。

更尴尬的事情在于这个CancelledError的抛出机制。asyncio内部使用了Python的生成器和yield from机制,yield from可以自动代理异常,

为了说明这一点我们考虑下面的代码:

import traceback
import asyncio

async def func1():
    try:
        return await func2()
    except Exception:
        traceback.print_exc()
        raise

async def func2():
    try:
        await asyncio.sleep(2)
    except Exception:
        traceback.print_exc()
        raise

async def func3():
    t1 = asyncio.ensure_future(func1())
    await asyncio.sleep(1)
    t1.cancel()
    try:
        await t1
    except CancelledError:
        pass

t1.cancel()这里,会发生什么呢?实际上异常会从最内层的func2开始抛出,从func2抛出到func1,再到func3的await t1,所以可以看到两次traceback打印。

这就是异步方法中await的异常代理机制,它像同步调用一样,有完整的堆栈,并且异常从最内层抛出。这本身是一个很好的设计,很方便调试,但是一旦CancelledError抛出,你是无法确定它具体从哪条语句抛出的,这样在写异步逻辑的时候,实际上必须假设所有的await语句都有可能抛出CancelledError。如果在外面加上了前面的do_not_raise这样的机制,就会错误地忽略掉CancelledError

所以异步逻辑中的忽略异常必须写成:

async def do_not_raise(user_defined_coroutine):
    try:
        await user_defined_coroutine
    except CancelledError:
        raise
    except Exception:
        logger.warning("User defined logic raises an exception", exc_info=True)
        # ignore

这样才能保证CancelledError不被错误捕获。

从这个结果上来看,CancelledError从一开始就不应该继承自Exception,它应该是一个BaseException,这样就可以减少很多异步编程中的错误。

并不是自己不调用cancel()就不会出现这样的问题。一些会触发cancel()过程的常见例子包括:

asyncio.wait_for在执行超时的时候会自动cancel内部的过程,这是一个很常用的实现超时逻辑的方法
aiohttp的handler,如果没有处理完成之前用户就关闭了HTTP连接(比如强制点了浏览器的停止按钮),会对handler的异步过程调用cancel()
……
还有更尴尬的事情,许多时候我们不得不捕获CancelledError。刚才的一段代码,我故意没有提,读者们是否发现问题了呢?

    t1.cancel()
    try:
        await t1
    except CancelledError:
        pass

在asyncio中,cancel()方法并不会立即结束一个异步Task,它只会抛出CancelledError,但是异步过程有机会使用except或者finally,在退出之前执行一些清理过程。这里的await的本意也是等待t1完全退出再继续。但是t1会抛出CancelledError,所以捕获这个异常,不让它再抛出。(而且如果不这么做,asyncio会打印一行warning,表示一个异步Task失败没有被处理)

那么问题就来了:如果func3()在执行到这里的时候,又被外部代码cancel()了呢?下面的except CancelledError就会变成问题,它会错误捕获外部的CancelledError。另外,t1也会再次被cancel一遍(没错,await一个Task的时候,如果await所在过程被cancel,Task也会被cancel,需要使用asyncio.shield来规避)

正确的写法应该是:

    t1.cancel()
    await asyncio.wait([t1])
    try:
        await t1
    except CancelledError:
        pass

asyncio.wait等待Task执行结束,但并不收集结果,因此内层的CancelledError不会在这里抛出来,而且如果此时取消func3,CancelledError并不会被忽略。第二个await t1时,t1可以保证已经结束,这里内部没有其他异步等待过程,因此CancelledError不会抛出在这里。也可以用t1.exception()之类代替。

到此这篇关于Python asyncio的一个坑的文章就介绍到这了,更多相关Python asyncio的一个坑内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Python中的asyncio代码详解

    asyncio介绍 熟悉c#的同学可能知道,在c#中可以很方便的使用 async 和 await 来实现异步编程,那么在python中应该怎么做呢,其实python也支持异步编程,一般使用 asyncio 这个库,下面介绍下什么是 asyncio : asyncio 是用来编写 并发 代码的库,使用 async/await 语法. asyncio 被用作多个提供高性能 Python 异步框架的基础,包括网络和网站服务,数据库连接库,分布式任务队列等等. asyncio 往往是构建 IO 密集型和

  • 详解python中asyncio模块

    一直对asyncio这个库比较感兴趣,毕竟这是官网也非常推荐的一个实现高并发的一个模块,python也是在python 3.4中引入了协程的概念.也通过这次整理更加深刻理解这个模块的使用 asyncio 是干什么的? 异步网络操作并发协程 python3.0时代,标准库里的异步网络模块:select(非常底层) python3.0时代,第三方异步网络库:Tornado python3.4时代,asyncio:支持TCP,子进程 现在的asyncio,有了很多的模块已经在支持:aiohttp,ai

  • Python asyncio的一个坑

    我们先从一个常见的Python编程错误开始说起,我已经见过非常多的程序员犯过这种错误了: def do_not_raise(user_defined_logic): try: user_defined_logic() except: logger.warning("User defined logic raises an exception", exc_info=True) # ignore 这段代码的错误之处在哪里呢? 我们从Python的异常结构开始说起.Python中的异常基类有

  • Python Asyncio调度原理详情

    目录 前言 1.基本介绍 2.EventLoop的调度实现 3.网络IO事件的处理 前言 在文章<Python Asyncio中Coroutines,Tasks,Future可等待对象的关系及作用>中介绍了Python的可等待对象作用,特别是Task对象在启动的时候可以自我驱动,但是一个Task对象只能驱动一条执行链,如果要多条链执行(并发),还是需要EventLoop来安排驱动,接下来将通过Python.Asyncio库的源码来了解EventLoop是如何运作的. 1.基本介绍 Python

  • 浅析python中SQLAlchemy排序的一个坑

    前言 SQLAlchemy是Python编程语言下的一款ORM框架,该框架建立在数据库API之上,使用关系对象映射进行数据库操作,简言之便是:将对象转换成SQL,然后使用数据API执行SQL并获取执行结果.最近在使用SQLAlchemy排序遇到了一个坑,所以想着总结下来,分享给更多的朋友,下面来一起看看吧. 坑的代码 query = db_session.query(UserVideo.vid, UserVideo.uid, UserVideo.v_width, UserVideo.v_heig

  • Python Asyncio库之asyncio.task常用函数详解

    目录 前记 0.基础 1.休眠--asyncio.sleep 2.屏蔽取消--asyncio.shield 3.超时--asyncio.wait_for 4.简单的等待--wait 5.迭代可等待对象的完成--asyncio.as_completed 前记 Asyncio在经过一段时间的发展以及获取Curio等第三方库的经验来提供更多的功能,目前高级功能也基本完善,但是相对于其他语言,Python的Asyncio高级功能还是不够的,但好在Asyncio的低级API也比较完善,开发者可以通过参考A

  • 使用Python来做一个屏幕录制工具的操作代码

    一.写在前面 作为一名测试,有时候经常会遇到需要录屏记录自己操作,方便后续开发同学定位.以前都是用ScreenToGif来录屏制作成动态图,偶尔的机会看到python也能实现.那就赶紧学习下. 二.效果展示 三.知识串讲 这次要讲的东西可能比较多了,涉及到pyqt5 GUI软件的制作.QThread多线程的使用.Sikuli库的图形操作.win32库的模拟键盘操作.cv2库的写视频文件等.下面我们一点点来蚕食我这次写的代码. 1.GUI界面制作 这次我用的是现成的Pyqt5界面布局类,QVBox

  • 在python里创建一个任务(Task)实例

    与事件循环进行交互,最基本的方式就是任务,任务封装了协程和自动跟踪它的状态.任务是Future类的子类,所以其它协程可以等待任务完成,或当这些任务完成获取返回结果. 在这里通过create_task()函数来创建一个任务实例,然后事件循环就运行这个任务,直到这个任务返回为止: import asyncio async def task_func(): print('in task_func') return 'the result' async def main(loop): print('cr

  • python asyncio 协程库的使用

    asyncio 是 python 力推多年的携程库,与其 线程库 相得益彰,更轻量,并且协程可以访问同一进程中的变量,不需要进程间通信来传递数据,所以使用起来非常顺手. asyncio 官方文档写的非常简练和有效,半小时内可以学习和测试完,下面为我的一段 HelloWrold,感觉可以更快速的帮你认识 协程 . 定义协程 import asyncio import time async def say_after(delay, what): await asyncio.sleep(delay)

  • Python  Asyncio模块实现的生产消费者模型的方法

    asyncio的关键字说明 event_loop事件循环:程序开启一个无限循环,把一些函数注册到事件循环上,当满足事件发生的时候,调用相应的协程函数 coroutine协程:协程对象,指一个使用async关键字定义的函数,它的调用不会立即执行函数,而是会返回一个协程对象,协程对象需要注册到事件循环,由事件循环调用. task任务:一个协程对象就是一个原生可以挂起的函数,任务则是对协程进一步封装,其中包含了任务的各种状态 future:代表将来执行或没有执行的任务结果.它和task上没有本质上的区

  • 关于Python 位运算防坑指南

    目录 1.背景 2.C# 语言 3.Python 语言 4.技术分析 1.背景 我们先看这个题目: 标题:137. 只出现一次的数字 II 难度:中等 https://leetcode-cn.com/problems/single-number-ii/ 给定一个 非空 整数数组,除了某个元素只出现一次以外,其余每个元素均出现了三次.找出那个只出现了一次的元素. 说明: 你的算法应该具有线性时间复杂度. 你可以不使用额外空间来实现吗? 示例 1: 输入: [2,2,3,2] 输出: 3 示例 2:

  • python安装cxOracle避坑总结不要直接pip install

    目录 到官网下载相应版本的驱动进行安装 1.安装过程中的错误: 2.命令行中运行提示找不到指定的模块 3.命令行中运行提示不是有效的win32模块 转自http://rookiefly.cn/detail/69 作死小能手这两天闲着没事,把自己电脑重装了,然而重装过后配置开发环境踩了一些坑,这里把安装cx_oracle遇到的坑记录下来,方便以后查看. 使用pip安装出现的问题 命令: pip install cx_oracle 错误: Unable to find vcvarsall.bat 我

随机推荐