线程 vs 异步

105

我一直在阅读关于线程模型和异步模型的编程,来自这篇非常好的文章。 http://krondo.com/blog/?p=1209

然而,这篇文章提到了以下几点。

  1. 每当有I/O操作时,异步程序将通过在任务之间切换来简单地优化同步程序。
  2. 线程由操作系统管理。

我记得读过线程由操作系统管理,通过在Ready-Queue和Waiting-Queue之间移动线程控制块(TCB)(以及其他队列)来实现。在这种情况下,线程也不会浪费等待时间,是吗?

基于上述内容,请问异步程序相对于线程程序有哪些优势?


5
不,我是指线程化与异步。我提到第一点只是因为那是我从文章中理解的内容。 - user277465
7个回答

120
  1. 编写线程安全代码非常困难,而使用异步代码,你可以准确地知道代码将从一个任务切换到下一个任务的位置,因此竞争条件要更加难以出现。
  2. 线程消耗相当多的数据,因为每个线程都需要拥有自己的堆栈。使用异步代码,所有代码共享同一堆栈,并且由于在任务之间不断展开堆栈,因此堆栈保持较小。
  3. 线程是操作系统结构,因此对于平台来说会占用更多的内存。异步任务没有这样的问题。

更新2022年:

许多语言现在支持无堆栈协程(async/await)。这使我们能够几乎同步地编写任务,同时在设置的位置(睡眠或等待网络或其他线程)中让出其他任务(await)。


18
稍作解释:
  1. 线程化代码的I/O部分相对容易,但是在没有竞争条件的情况下管理线程间的共享状态(使用锁/队列等)是让它变得棘手的。使用异步模型意味着您同时处理的内容较少,因此竞争条件很容易避免。 2 / 3. 每个线程将使用至少一页内存栈(通常为4KB或8KB),加上一些与该线程状态相关的其他数据结构所需的未知数量的内存。
- Dobes Vandermeer
再加一项:使用res = await task更容易获取、共享和理解延迟任务的结果,而在单独的线程中执行代码需要使用共享对象(如队列)来获取结果。 - MjH

16

14
  1. 假设您有2个任务,不涉及任何IO(在多处理器机器上)。 在这种情况下,线程优于Async。因为Async像单线程程序一样按顺序执行您的任务。但是线程可以同时执行两个任务。

  2. 假设您有2个涉及IO的任务(在多处理器机器上)。 在这种情况下,Async和Threads表现出更或多或少相同的性能(性能可能会因核心数、调度、任务的进程密集程度等而有所不同)。此外,与多线程程序相比,Async需要更少的资源、低开销和较少的编程复杂性。

它如何工作? 线程1执行任务1,因为它正在等待IO,所以被移到IO等待队列中。类似地,线程2执行任务2,因为它也涉及IO,所以被移到IO等待队列中。一旦其IO请求得到解决,它就被移到准备队列中,以便调度程序可以安排线程进行执行。

Async执行任务1,而不等待其IO完成,然后继续执行任务2,然后等待两个任务的IO完成。它按IO完成的顺序完成任务。

Async最适合涉及Web服务调用、数据库查询调用等任务, 线程适用于进程密集型任务。

下面的视频介绍了Async vs Threaded model以及何时使用等, https://www.youtube.com/watch?v=kdzL3r-yJZY

希望这有所帮助。


4
欢迎提供潜在解决方案的链接,但请添加链接周围的上下文,这样其他用户就能大致了解它是什么以及为什么存在。始终引用重要链接的最相关部分,以防目标网站无法访问或永久关闭。请注意,仅仅是一个外部链接可能是为什么有些答案会被删除?的原因之一。 - Machavity
@Lakshmipathi,您能否详细说明一下您的(1.示例)?我认为您提出的多线程实现也利用了多进程。问题在于,您也可以在单个核心上拥有多个线程,在我看来,您的第一个示例有些误导性。 - Gr3at
原始问题有一个“python”标签,答案中的1由于GIL不适用。 - MjH

6
首先,需要注意的是线程的实现和调度的详细细节在不同操作系统中可能会有所不同。一般来说,你不需要担心线程之间的等待,因为操作系统和硬件会尝试安排它们以便在单处理器系统上异步运行,在多处理器上并行运行时能够高效运行。
一旦线程完成了等待某些事情,比如I/O操作,它就可以被认为是可运行的。可运行的线程将在不久的将来被安排执行。这是否作为一个简单队列或者更复杂的东西实现,也取决于操作系统和硬件。你可以把阻塞线程看作是一个集合而不是一个严格排序的队列。
需要注意的是,在单处理器系统上,按照此处定义的异步程序相当于线程化程序。

1

请参见 http://en.wikipedia.org/wiki/Thread_(computing)#I.2FO_and_scheduling

然而,在用户线程(相对于内核线程)或纤程中使用阻塞系统调用可能会有问题。如果用户线程或纤程执行了一个阻塞的系统调用,进程中的其他用户线程和纤程将无法运行,直到系统调用返回。这个问题的典型例子是当执行 I/O 时: 大多数程序都是同步执行 I/O 的。当启动 I/O 操作时,会进行系统调用,并且在 I/O 操作完成之前不会返回。在此期间,整个进程被内核"阻塞",不能运行,从而使得同一进程中的其他用户线程和纤程无法执行。

根据此情况,当一个线程在 IO 中被阻塞时,整个进程可能会被阻塞,没有线程会被调度。我认为这取决于操作系统,并不总是成立。


1
公正地说,相比异步方法,使用CPython GIL下的线程有以下好处:
  1. 首先编写具有单一事件流(无并行执行)的典型代码更容易,并且可以在单独的线程中运行多个副本:它将使每个副本都保持响应性,而自动实现所有I/O操作的并行执行将带来好处;
  2. 许多经过时间验证的库都是同步的,因此很容易包含在线程版本中,而不是异步版本中;
  3. 一些同步库实际上在C级别上释放了GIL,从而允许超出I/O限制的任务并行执行,例如NumPy;
  4. 通常编写异步代码更加困难:包含重型CPU绑定部分会使并发任务不响应,或者可能会忘记等待结果并提前完成执行。
因此,如果没有立即计划将服务扩展到超过约100个并发连接,则最好从线程版本开始编写,然后再使用其他更高效的语言(如Go)重新编写。

0

异步 I/O 意味着驱动程序中已经有一个线程在执行任务,因此您正在复制功能并产生一些开销。另一方面,通常没有文档说明驱动程序线程的确切行为,在复杂的情况下,当您想要控制超时/取消/启动/停止行为、与其他线程同步时,实现自己的线程是有意义的。有时候以同步方式进行推理也更容易。


6
这并不是异步 I/O 的工作方式。从根本上讲,I/O 是事件驱动的(你发起一个对设备的 I/O 请求,稍后,设备会完成它,并希望通过中断告诉你)。某些类型的 I/O(例如磁盘 I/O)可能由于某些比较难以理解的原因而使用内核线程;但对于网络来说,始终都是异步操作。 - Glyph

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