Python:异步与线程相比有哪些优势?

54

我一直很难理解Python中异步功能的实现和原因,特别是“为什么”部分。如果我错了,请纠正我。

异步方法和线程的目的都是使多个任务可以同时处理。

线程方法看起来简单而直观。如果Python程序要并发处理多个任务,我们为每个任务分配一个线程(可以有子线程),每个线程的堆栈反映对应任务的当前处理阶段。一切都很简单,有易于使用的机制来启动新的线程并等待其结果。

据我所知,这种方法唯一的问题就是线程开销很大。

另一种方法是使用async协程。我能看到几个不便之处。我只列举其中的两个。现在我们有两种类型的方法:普通方法和async方法。90%的时间唯一的区别是你需要记住这个方法是async的,并且在调用此方法时不要忘记使用await关键字。是的,你不能从普通方法中调用async方法。而所有这些async-await的语法垃圾只是为了表明这个方法能够将控制权让给消息循环。

线程方法没有这些不便之处。但是,async-await方法能够处理比线程方法更多的并发任务。这是怎么可能的呢?

对于每个并发任务,我们仍然有一个调用堆栈,只不过现在它是一个协程调用堆栈。我不太确定,但看起来这是关键差异:普通堆栈是操作系统堆栈,它们很昂贵,协程堆栈只是Python结构,它们要便宜得多。我的理解正确吗?

如果这是正确的,那么是否更好地将Python线程/调用堆栈与操作系统线程/调用堆栈分离,以使Python线程更便宜?

对不起,如果这个问题很蠢的话。我相信使用async-await方法有一些原因。只是想了解这些原因。

更新:

对于那些认为这个问题不好且太宽泛的人。

这里有一篇文章Unyielding——它以解释线程为何不好并推广async方法开头。主旨:线程是邪恶的,很难确定一个可能从任意数量的线程同时执行的例程。

感谢 Nathaniel J. Smith(Python Trio库的作者)推荐这个链接。

顺便说一句,文章中的论点对我来说并不令人信服,但仍然可能有用。


4
我把它写成评论是因为它并不构成一个完整的答案:async基于协作调度。你可以确定哪些代码块会在没有中断的情况下执行。你也可以确信异步代码在运行时不会与其他代码进行交互,因为没有任何东西会并行执行,不像线程。这使得诸如同步、锁定和竞态条件等问题变得更容易处理。 - VPfB
@VPfB 谢谢,好观点,我没有想到。有趣的是,上周我不得不使用 threading.Lock,尽管应用程序是基于 async 的。代码的“关键部分”正在与外部应用程序通信,很重要的是,来自不同协程的请求系列不会在时间上重叠。因此,即使没有线程,同步问题也不会完全消失。 - lesnik
多进程 vs 多线程 vs asyncio - Benyamin Jafari
2个回答

20

这篇文章回答了你的问题。

简而言之:

Python中的线程非常低效,因为有全局解释器锁(GIL),这意味着在多处理器系统上不能像期望的那样并行运行多个线程。此外,你必须依赖解释器来切换线程,这会增加不必要的开销。

异步/asyncio允许在单个线程内实现并发操作。这让你作为开发人员更加精细地控制任务切换,并且对于I/O密集型的并发任务,相对于Python线程,异步可以提供更好的性能。

第三种方法是使用多进程实现并发性,允许程序充分利用具有多个核心的硬件。


不,这并不是太长不看!但我需要一些时间来阅读这篇文章。谢谢回答! - lesnik
@lesnik,这个回答解决了你的问题吗? - Luke Smith
20
GIL是一个问题,但它并不能解释为什么asyncio比线程更好。它解释了为什么线程不比asyncio好很多。因为GIL(做了简化),每次只能运行一个Python线程。asyncio的情况也是如此,每次只有一个任务在运行。 还有一个论点:线程调度器(或分派器或选择器)不够好,可能会选择被阻塞在IO操作上的线程。我不确定这是否正确(请参见对第二个答案的Dunes评论)。这篇文章没有回答我的问题 :(。 - lesnik
我推荐你试试这本书:https://learning.oreilly.com/library/view/using-asyncio-in/9781492075325/cover.html - Hardik Ojha

3
异步编程是一种完全不同的世界,据我所知,它是 Python 对 Node.js 的应答,自从开始以来就实现了这些功能。例如,关于 asyncio 的官方 Python 文档指出:

异步编程与传统 “顺序” 编程不同。

因此,您需要决定是否想要深入了解这些术语。如果您面临网络或磁盘相关的重任务,则可能只有这样才有意义。如果是这样,那么例如这篇文章声称 Python 3 的 asyncio 可能比 node.js 更快,并且接近 Go 的性能。
话虽如此:我还没有使用过 asyncio,所以我无法真正评论这个问题,但我可以评论您问题中的几个句子:

而所有这些围绕程序的 async-await 语法垃圾只是为了表明该方法能够将控制权交给消息循环

据我所见,您有一个 asyncio 的初始设置,但是随后所有调用的语法都比使用线程进行相同操作时更少。您需要 start()join(),并且可能还需要使用 is_alive() 进行检查,并且要获取返回值,则需要先设置共享对象。因此:没有,asyncio 看起来只是不同而已,但最终程序可能比线程更清晰。

我认为这种方法的唯一问题就是线程很昂贵

不完全是。启动新线程非常便宜,据我所知,其成本与在 C 或 Java 中启动“本机线程”相同。

看来这是关键区别:通常的堆栈是操作系统堆栈,它们很昂贵,协程堆栈只是 Python 结构,它们要便宜得多。我的理解是正确的吗?

不完全是。没有什么能比创建 OS 级别的线程更好了,它们很便宜。 asyncio 更擅长的是您需要更少的线程切换。因此,如果您有许多并发线程等待网络或磁盘,则 asyncio 可能会加快进展速度。

我认为你对绿色线程有些困惑,特别是在同一句话中提到内核。CPython默认不使用绿色线程(这就是asyncio、gevent、PyPy等库的作用)。CPython使用pthread或nt线程,取决于操作系统。由于GIL只有一个线程在解释,这并不意味着线程是绿色的(它只是给它们赋予了类似绿色的属性)。在CPython中并行是可能的,只是Python字节码不能并行化。任何释放GIL的扩展库,如numpy,都可以并行运行。 - Dunes
@Dunes,是的,现在我很困惑。我原本认为Python确实会将pthread转换为“绿色线程”,因为它会从操作系统中取走调度程序并自己扮演调度程序,从而引入GIL。这样说有问题吗? - hansaplast
顺便说一句:暂时删除了“绿色线程”段落,因为Dunes很可能是正确的,我在那里混淆了事情。 - hansaplast
1
很奇怪。调度仍由操作系统管理。但是,由于GIL的存在,任何被调度且未持有锁的线程都无法执行任何操作(除了表现良好的扩展库)。因此,操作系统和CPython必须就要运行哪个线程达成一致(而操作系统会在随机选择)。这是GIL的主要缺点。但是,在CPython中进行IO的线程还可以,因为操作系统知道不要安排任何正在等待尚未完成的IO的线程。(CPython在实际进行IO的系统调用之前释放GIL)。 - Dunes
谢谢你的回答,但我不同意你的观点(至少在比较代码复杂性时)。x = await get_x() 的线程模拟只是 x = get_x() - 你的线程会一直阻塞,直到 x 的值准备好。实现类似于“启动多个任务并等待它们全部准备就绪”的代码复杂度在两种方法中大致相同。 - lesnik

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