异步IO与Gevent的比较

69

背景

我曾经在一个使用Python2编写的系统上工作过,其中有很多自定义的同步I/O代码,并且使用线程进行扩展。某个时刻,我们无法进一步扩展它,意识到我们必须转向异步编程。

  • Twisted 是一个受欢迎的选择,但我们想避免其回调地狱。
  • 它确实有@inlineCallbacks修饰符,有效地使用生成器魔法实现协程,一些其他库也是如此。那更容易被容忍,但感觉有点不太稳定。
  • 然后我们发现了gevent。你所要做的就是:
from gevent import monkey
monkey.patch_all()

就像这样,你所有的标准I/O、套接字、数据库事务以及所有纯Python编写的内容都是异步的,使用greenlets在幕后进行yield和切换。

它并不完美:

  • 那时,在Windows上它工作得不好(今天它仍然有一些限制)。幸运的是,我们正在运行Linux系统。
  • 它无法monkey-patch C扩展库,因此我们不能使用MySQLdb。幸运的是,有许多纯Python的替代品,比如PyMySQL。

问题

现在,Python 3更受欢迎,而伴随着它的是asyncio。个人认为它很棒,但最近有人问我它与我们用gevent实现的方式有哪些不同,并且我没有想到足够好的答案。

这听起来可能很主观,但实际上我正在寻找一个真实的应用场景,在这个场景中,一个会明显超过另一个,或者允许其他不能做的事情。以下是我迄今为止收集到的考虑:

  1. 就像我所说的,gevent在Windows上有一些限制。不过,我知道的大多数生产代码都运行在Linux上。

    如果你需要在Windows上运行,请使用asyncio

  2. Gevent无法monkey-patch C扩展库。但是,asyncio无法monkey-patch 任何东西。

    想象有一个新的数据库技术出现了,你想使用它,但是没有用于它的纯Python库,所以你无法将其集成到Gevent中。问题是当没有一个io*库可以与asyncio集成时,你也是被卡住的!当然有工作线程和执行器,但这不是重点,而且在两种情况下效果都很好。

  3. 有些人说这是个人口味的问题,但我认为同步编程本质上比异步编程更容易(想一想:你是否曾经遇到过一个初学者程序员,他可以使用套接字,但很难正确理解如何选择/轮询,或者思考future/promise?反之亦然)。

    无论如何,我们不会深入讨论这一点。我想提到这一点是因为它经常出现(这里有一个reddit上的讨论),但我真正想知道的是何时使用其中之一具有实际原因。

  4. asyncio是标准库的一部分。这非常重要:这意味着它得到了很好的维护,很好地记录,并且每个人都知道它并默认使用它。

    但是,考虑到你需要了解多少Gevent才能使用它(而且它也很好地得到了维护和记录),似乎并不是很关键。因此,虽然即使涉及到future的最复杂的情况,StackOverflow上也有多个答案,但完全不使用future的可能性似乎同样可行。


2
我并不认为这是客观的,但我不会投票关闭它作为“主要观点”。我建议您可以削减很多闲聊内容,使其更加客观。 - roganjosh
当然,在StackOverflow上有多个答案,即使涉及到未来最复杂的情况,这很好 - 但更好的是根本不需要使用未来。这怎么可能不是主观的呢? - roganjosh
感谢您的理解!我认为“异步编程比同步编程更难”或者“future 是一项高级功能,不必使用它很好”这样的说法是受到了一个关于元类具体用途的问题的鼓励(https://dev59.com/OnRC5IYBdhLWcg3wKtv2#31061875)。就像 asyncio 一样,它是 Python 中的标准和有用的功能,但相对而言,可以说它比讨论的其他替代方案更加高级。 - Dan Gittik
1
哦,好的。你显然给出了一个非常详细的答案,但是自那个问题被提出以来,SO的标准已经发生了很大变化。仍然只是我的意见,但我担心除非你缩减它,否则你会面临关闭投票;当然,这取决于其他人和你是否想改变文本。无论如何,我没有能力给你一个合适的答案。 - roganjosh
我欣赏您的坦诚。我会尽可能地减少它。 - Dan Gittik
1个回答

35

实际应用中的简单答案:

  1. gevent的优点是可以打补丁,这意味着你理论上可以使用同步库。例如你可以打django的补丁。
  2. gevent的缺点是不能打补丁的东西也有,如果你必须使用某些无法打补丁的DB驱动程序,那就注定了。
  3. gevent最糟糕的一点是它很“神奇”。理解“patch_all”发生的事情需要大量的努力,招聘新人也一样。更糟糕的是,基于gevent的代码调试起来非常困难。我认为,除了回调之外,几乎和回调一样难。

后面的观点是关键。软件工程中最被低估的事情是代码应该被阅读而不是有效地编写或运行(如果后者是目的,你最好从Python切换到系统级语言)。Asyncio提供了异步编程的缺失部分——预定义和可控制的上下文切换点。实际上,你编写的是同步代码(也就是说,你不考虑突然的线程切换、锁、队列等),并在知道调用是IO阻塞时使用await ...,这样你可以让事件循环选择其他已经准备好CPU的东西,并稍后恢复当前状态。

这就是asyncio如此出色的原因——易于维护。缺点是几乎所有的“世界”也必须是异步的——数据库驱动程序、http工具、文件处理程序等。有时你会发现缺少一些库,这几乎可以确定。


17
我不确定我理解这个比较。你不需要像了解asyncio的实现一样了解patch_all的作用;你只需要编写简单、易读和可维护的同步代码(并像调试同步代码一样调试它),然后神奇地将其变成异步代码。我漏掉了什么? - Dan Gittik
2
你不能像同步代码一样调试已打补丁的代码,这就是关键所在。使用单用户负载和100k(例如)运行gevent打了补丁的代码是不同的——在上下文何时以及如何切换方面。asyncio的实现(肯定很难)与理解事件循环的概念是不同的。后者足够简单。 - Slam
1
@DanGittik说:“你只是写同步代码,然后神奇地将其变成异步”——如果你尝试这样做,你不会从使代码异步化中获得任何好处。你必须同时运行一些任务(协程)才能获得好处(例如gevent如何实现)。并发性出现后,协程之间的同步迟早会出现,像死锁和许多其他异步特定问题。这些通常是非常复杂的事情,而asyncio允许您更好地阅读和调试它们,而不是采用“神奇”的方法。 - Mikhail Gerasimov
4
这不是最糟糕的事情,而是应该的工作方式。在 Golang 中,所有内容都是异步进行的,它只会正常工作。而在 Python 中,有多种选择,但它们每一种都有不同的问题。只需比较 subprocess 和 asyncio.subprocess,即可看到标准库提供了两种不具备功能平衡性的完成相同任务的方法。这就是我所说的问题所在。 - Kostja
2
是的,不。所有异步操作只是在每个地方添加async/await,你仍然面临着相同的问题,需要解决协程的执行。所有额外的标记都不能使其更易读或易懂。更好的方法是简单地识别和记录异步入口点。 - Blaze
显示剩余4条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接