iis服务器助手广告广告
返回顶部
首页 > 资讯 > 后端开发 > Python >Python asyncio的一个坑
  • 883
分享到

Python asyncio的一个坑

2024-04-02 19:04:59 883人浏览 安东尼

Python 官方文档:入门教程 => 点击学习

摘要

我们先从一个常见的python编程错误开始说起,我已经见过非常多的程序员犯过这种错误了: def do_not_raise(user_defined_logic): tr

我们先从一个常见的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内部的过程,这是一个很常用的实现超时逻辑的方法
aioHttphandler,如果没有处理完成之前用户就关闭了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的一个坑内容请搜索编程网以前的文章或继续浏览下面的相关文章希望大家以后多多支持编程网!

--结束END--

本文标题: Python asyncio的一个坑

本文链接: https://www.lsjlt.com/news/159763.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • Python asyncio的一个坑
    我们先从一个常见的Python编程错误开始说起,我已经见过非常多的程序员犯过这种错误了: def do_not_raise(user_defined_logic): tr...
    99+
    2024-04-02
  • Java中HashMap中的一个坑
    目录前言问题展示原因分析解决方案LinkedHashMap 的魔力总结前言 最近公司的系统要增加一个新的列表展示功能,功能本身难度并不大,但遇到了一个很“奇怪&rdquo...
    99+
    2024-04-02
  • 解决Golang中ResponseWriter的一个坑
    在使用Context.ResponseWriter中的Set/WriteHeader/Write这三个方法时,使用顺序必须如下所示,否则会出现某一设置不生效的情况。 ctx.Re...
    99+
    2024-04-02
  • Python asyncio的示例分析
    Python asyncio的示例分析,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。我们先从一个常见的Python编程错误开始说起,我已经见过非常多的程序员犯过这...
    99+
    2023-06-22
  • 使用fileReader的一个坑及解决
    目录关于fileReader的一个坑fileReader在ios上面的坑(图片转base64)关于fileReader的一个坑 在用fileReader做图片浏览时, 使用base6...
    99+
    2024-04-02
  • 一个@Component注解引发的大坑
    目录一个@Component注解引发的大坑问题是这样的思考对spring @component注解的理解@Component注解的使用注解的类测试的类一个@Component注解引发...
    99+
    2024-04-02
  • MySQL UPDATE 语句一个“经典”的坑
    目录1、有问题的SQL语句有人问,比如下图: 问题归纳起来就是:在MySQL里面update一条记录,语法都正确的,但记录并没有被更新... 刚遇到这个问题的时候,我拿到这条语句直...
    99+
    2024-04-02
  • 使用@JsonFormat的一个坑及解决
    目录使用@JsonFormat的一个坑及解决解决如下JsonFormat的时间格式踩坑使用@JsonFormat的一个坑及解决 spring boot项目 ,mysql数据库的dat...
    99+
    2024-04-02
  • python中关于os.path.pardir的一些坑
    目录关于os.path.pardir的一些坑python中os.path常用模块1 BASE_DIR变量2 os.listdir()的用法3 如下所示4 os.path.splite...
    99+
    2024-04-02
  • python 中的 asyncio 异步协程
    目录一、定义协程二、运行协程三、协程回调四、运行多个协程五、run_forever六、多协程中关闭run_forever一、定义协程 asyncio 执行的任务,称为协程,但是Asy...
    99+
    2024-04-02
  • 我实在不懂Python的Asyncio
    原语 事件循环(Event Loop) Awaitables和Coroutines Coroutine Wrappers Awaitables and Futures Tasks Handles Executors Transpor...
    99+
    2023-01-31
    不懂 我实在 Asyncio
  • 怎么在Python中利用 Asyncio模块实现一个生产消费者模型
    本文章向大家介绍怎么在Python中利用 Asyncio模块实现一个生产消费者模型的基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。Python主要用来做什么Python主要应用于:1、Web开发;2、数...
    99+
    2023-06-06
  • 向大家分享一个shell脚本的坑
    打算在跳板机上写一个shell脚本,批量检查远程服务器上的main进程是否在健康运行中。先找出其中一台远程机器,查看main进程运行情况[root@two002 tmp]# ps -ef|grep m...
    99+
    2023-06-06
  • sql server中的一个坑-len与datalength区别
    今天在处理问题时,统计一个字段最大字节数时,出现了问题: select max(len(subject_name)) from dbtabletest; 但返回值为129。 ...
    99+
    2024-04-02
  • 浅谈vue-cli5关于yarn的一个小坑
    目录问题解决方案原因问题 昨天有小伙伴下了我的 DEMO之后反映运行报错。 因为这个项目环境我测试过许多次,不管是npm还是yarn都能正常运行,所以听到运行报错时下意识地就认为是...
    99+
    2024-04-02
  • 解决spring boot 配置文件后缀的一个坑
    目录spring boot 配置文件后缀的一个坑spring boot配置文件支持 properties和yml从新创建一个demo试试 spring boot 配置文件后缀导致启动...
    99+
    2024-04-02
  • Redis分布式锁一定要避开的两个坑
    目录1 第一个坑:错误释放锁时机1.1. 发现问题1.2 解决问题2 第二个坑:缓存失效问题2.1 过期清理机制2.2 内存回收机制2.3 乐观锁1 第一个坑:错误释放锁时机 1.1. 发现问题 分析以下代码存在什么问题...
    99+
    2023-04-13
    Redis分布式锁避坑 Redis分布式锁
  • 一个Python的‘Threadpool
        为了能够实现异步操作获得目标主机的响应头中“Server”字段信息,准备用requests+ThreadPool进行。于是写了两个函数,一个请求,一个负责记录。具体代码片段如下: def getHead(port,ipss):  ...
    99+
    2023-01-31
    Python Threadpool
  • 一个关于JS正则匹配的踩坑记录
    最近发现在JS里的正则匹配有一个坑,而且当时很莫名奇妙,一度让我怀疑出现了灵异事件。 下面是踩坑代码 var str=["二七1","二七2","金水","二七3","二七...
    99+
    2024-04-02
  • Swift踩坑实战之一个字符引发的Crash
    最近因为一个字符引发了 Crash,因为实际的业务场景不便描述,这里便用一段测试代码作说明。 话不多说,直接上代码: let testCharacters: Set<Chara...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作