多进程 vs 多线程 vs 异步IO

325

我发现在Python 3.4中,有几个不同的多进程/线程库可以使用:multiprocessing vs threading vs asyncio

但是我不知道应该使用哪一个或者哪一个是“推荐的”。它们是否做相同的事情,还是不同的?如果是这样的话,每个库用于什么?我想编写一个在我的计算机上利用多核的程序,但是我不知道应该学习哪个库。


4
也许《我对AsyncIO太蠢了》可以帮到你。 - Martin Thoma
1
我太蠢了,不适合使用AsyncIO。该网站已经下线,但仍可在以下链接中找到:https://web.archive.org/web/20210801000000*/https://whatisjasongoldstein.com/writing/im-too-stupid-for-asyncio/同时,建议阅读以下优秀的回答: https://medium.com/@pgjones/understanding-asyncio-a6592a517def - bluppfisk
2
工作链接到archive.org 我太蠢了,无法理解AsyncIO - radioxoma
11个回答

298

简而言之

做出正确的选择:

我们已经讨论了最常见的并发形式。但问题仍然存在 - 何时应该选择哪种形式?这真的取决于使用情况。根据我的经验(和阅读),我倾向于遵循这个伪代码:

if io_bound:
    if io_very_slow:
        print("Use Asyncio")
    else:
        print("Use Threads")
else:
    print("Multi Processing")
  • CPU Bound => 多进程处理
  • I/O Bound, 快速 I/O, 连接数有限 => 多线程处理
  • I/O Bound, 慢速 I/O, 连接数很多 => Asyncio 处理

参考链接


[]:

  • 如果您的程序中包含长时间的调用方法(例如包含睡眠时间或者是惰性 I/O 的方法),最佳选择是使用 asyncioTwistedTornado 的协程方法,这些方法仅使用单个线程进行并发处理。
  • Asyncio 适用于 Python3.4 及以上版本。
  • TornadoTwisted 从 Python2.7 开始支持。
  • uvloop 是一个超快的 asyncio 事件循环(使用 uvloop 可以使 asyncio 的速度提升 2-4 倍)。

[更新(2019)]:

  • Japranto (GitHub) 是一个基于 uvloop 构建的高效 pipelining HTTP 服务器。

3
如果我有一个请求的 URL 列表,使用 Asyncio 更好吗? - mingchau
5
@mingchau,是的,但请记住,在使用可等待函数时,您可以使用asynciorequest库不是一个可等待的方法,您可以使用aiohttpasync-request等库代替。请注意,不要改变原来的意思。 - Benyamin Jafari
4
请扩展一下有关如何使slowIO和fastIO支持多线程或asyncio的内容。 - droid192
3
请问io_very_slow具体是什么意思? - variable
15
@variable I/O绑定意味着你的程序大部分时间都在与慢速设备进行通信,比如网络连接、硬盘、打印机或者有睡眠时间的事件循环。所以在阻塞模式下,你可以选择线程或者asyncio,如果你的绑定部分非常慢,协作式多任务(asyncio)是更好的选择(即避免资源匮乏、死锁和竞态条件)。 - Benyamin Jafari
显示剩余5条评论

159
它们旨在满足(稍微)不同的目的和/或要求。CPython(典型的主流Python实现)仍然有全局解释器锁定,因此多线程应用程序(现在实现并行处理的标准方式)是次优的。这就是为什么multiprocessing 可能threading更受欢迎的原因。但并非每个问题都可以有效地分成[几乎独立的]部分,因此可能需要进行大量的进程间通信。因此,multiprocessing在一般情况下可能不如threading受欢迎。 asyncio(这种技术不仅在Python中可用,其他语言和/或框架也有,例如Boost.ASIO)是一种有效处理许多同时来源的I/O操作而无需并行代码执行的方法。因此,它只是一个特定任务的解决方案(确实是一个很好的解决方案!),而不是针对并行处理的一般性解决方案。

19
请注意,虽然这三种方法可能不能实现并行处理,它们都能够执行并发(非阻塞)任务。 - sargas
谢谢。您能详细解释一下“处理许多来自许多同时源的I/O操作,而无需并行代码执行”的含义吗? - Avv
请查看aiohttp包(教程在这里),以了解如何处理多个I/O操作而不使用(许多)线程。实际上,aiohttp基于协程,在回答创建之后完全引入了Python(asyncio的第一个版本在3.4 Python中首次出现,并在2015年底的3.5中完全成形...)。 - user3159253
1
然而,类似的技术早在90年代末/2000年代初就已经存在了,远在协程成为基于Python开发的主流技术之前,例如在Twisted中。其思想是围绕事件循环构建应用程序,使用非阻塞I/O操作,在主线程(或有限的线程池)内快速执行所有短期活动,并将长时间运行的操作(如大量计算)放到外部工作器中,以非阻塞方式收集它们的结果... - user3159253

98

multiprocessing 中,您可以利用多个CPU来分配计算任务。由于每个CPU都可以并行运行,因此您实际上可以同时运行多个任务。您需要使用multiprocessing来处理CPU-bound任务。例如,尝试计算巨大列表中所有元素的总和。如果您的计算机有8个内核,您可以将该列表“切分”成8个较小的列表,并在不同的核心上分别计算每个列表的总和,然后将这些数字相加即可。通过这样做,您将获得近8倍的加速效果。

在(multi)线程中,您不需要多个CPU。想象一个向网络发送大量HTTP请求的程序。如果使用单线程程序,则会在每个请求处停止执行(阻塞),等待响应,然后在收到响应后继续执行。问题在于,当等待一些外部服务器完成任务时,您的CPU实际上并没有做任何有用的工作,而它实际上可以在此期间进行一些有用的工作! 解决方法是使用线程-您可以创建许多线程,每个线程负责从Web请求某些内容。线程的好处在于,即使它们在一个CPU上运行,CPU也会不时地“冻结”一个线程的执行并跳转到执行另一个线程(这称为上下文切换,并且在非确定性间隔中不断发生)。因此,如果您的任务是I/O bound(即输入/输出受限),请使用线程。 asyncio 本质上就是线程,只不过不是CPU而是您作为程序员(或实际上是您的应用程序)决定何时以及在哪里进行上下文切换。在Python中,您使用await关键字来暂停协程的执行(使用async关键字定义)。

1
我不确定我是否理解了问题。这是关于当响应变快时是否应该使用多个核心吗?如果是这种情况-它取决于响应速度有多快以及您实际花费多少时间等待它们与使用CPU。如果您大部分时间都在进行CPU密集型任务,那么将其分布在多个核心上将是有益的(如果可能)。如果问题是系统是否会在“意识到”其工作是CPU绑定后自动切换到并行处理-我认为不会-通常需要明确告诉它这样做。 - Tomasz Bartkowiak
@TomaszBartkowiak 你好,我有一个问题:我有一个实时人脸识别模型,从网络摄像头输入并显示用户是否在场。由于处理速度较慢,存在明显的延迟,因为没有所有帧都是实时处理的。如果我创建10个线程来处理10帧而不是在一个线程上处理这10帧,你能告诉我多线程是否可以帮助我吗?仅说明一下,我的意思是,Keras上有一个经过训练的模型,它以图像帧为输入,并输出是否检测到人员。 - Talal Zahid
1
@TalalZahid,你的任务似乎是CPU密集型的 - 只有机器(CPU)执行推理(检测),而不是等待IO或其他人完成工作的一部分(即调用外部API)。因此,多线程处理没有意义。如果处理给定帧需要相当长的时间(是吗?)并且每个帧都是独立的,则可以考虑在单独的机器/核心上分布检测。 - Tomasz Bartkowiak
18
我喜欢你提到在async中开发者控制上下文切换,而在threading中操作系统控制上下文切换的方式。 - Arkyo
1
@Arkyo,从技术上讲,操作系统控制所有的上下文切换,但是在线程中,操作系统实现了抢占式多任务处理,因此它会根据各种算法自行决定何时进行切换。而在asyncio中,操作系统实现了协作式多任务处理,因此它会与正在运行的线程合作,并在该线程发出信号表明准备放弃CPU控制权时切换上下文。 - ruslaniv
显示剩余7条评论

44
这是基本思想:

它是IO密集型的吗?------------>使用asyncio

它是CPU密集型的吗?---------->使用multiprocessing

否则?---------------------------->使用threading

因此,基本上除非您遇到IO/CPU问题,否则请坚持使用线程。

14
你可能会遇到的第三个问题是什么? - EralpB
3
不是输入/输出或CPU受限的情况,就像一个线程工作者执行简单的计算或在本地读取数据块或从快速本地数据库中读取。或者只是睡觉和看东西。基本上,除非你有一个网络应用程序或重度计算,否则大多数问题都符合这个标准。 - Farshid Ashouri
1
如果你正在进行任何类型的计算,那么这是一个CPU绑定问题,所以你应该使用多进程,如果很简单,你可能不需要任何并发解决方案。在本地或从数据库读取数据的情况下,这是一个IO绑定问题,因此线程或asyncio都可以帮助你。两者之间的主要区别在于,在asyncio中,你比线程有更多的控制权,并且线程对你的程序有初始化成本,因此如果你计划使用大量线程,也许asyncio更适合你。我认为除了这些问题,我们没有其他类型的问题。 - CAIO WANDERLEY
我对线程中的IO概念有些困惑。有哪些IO问题的例子? - Arjuna Deva
@FarshidAshouri。那么,一个服务器接收帧以检测人脸并将结果发送回客户端,这不适用于“线程”类别,对吗? - Avv
@ArjunaDeva。一个例子正在等待来自互联网连接的请求。 - Avv

41
许多答案提供了如何选择仅1个选项的方法,但为什么不能同时使用所有3个选项呢?在本答案中,我将解释如何使用asyncio来管理组合所有3种并发形式,并且如果需要,稍后可以轻松切换

简短的答案


许多首次接触Python并发的开发人员最终会使用processing.Processthreading.Thread。然而,这些是低级API,已经通过concurrent.futures模块提供的高级API合并在一起。此外,生成进程和线程会产生开销,例如需要更多内存,这是一个我下面展示的示例所遇到的问题。在一定程度上,concurrent.futures会为您管理这个问题,因此您不能像生成几个进程并且每次完成后只重复使用这些进程那样轻易地生成一千个进程并使您的计算机崩溃。

这些高级API是通过concurrent.futures.Executor提供的,随后由concurrent.futures.ProcessPoolExecutorconcurrent.futures.ThreadPoolExecutor实现。在大多数情况下,应该使用这些而不是multiprocessing.Processthreading.Thread,因为当您使用concurrent.futures时,从一个API切换到另一个API更容易,并且您不必学习每个API的详细差异。

由于它们共享统一的接口,因此您还会发现使用multiprocessingthreading的代码通常会使用concurrent.futuresasyncio也不例外,可以通过以下代码使用:

import asyncio
from concurrent.futures import Executor
from functools import partial
from typing import Any, Callable, Optional, TypeVar

T = TypeVar("T")

async def run_in_executor(
    executor: Optional[Executor],
    func: Callable[..., T],
    /,
    *args: Any,
    **kwargs: Any,
) -> T:
    """
    Run `func(*args, **kwargs)` asynchronously, using an executor.

    If the executor is None, use the default ThreadPoolExecutor.
    """
    return await asyncio.get_running_loop().run_in_executor(
        executor,
        partial(func, *args, **kwargs),
    )

# Example usage for running `print` in a thread.
async def main():
    await run_in_executor(None, print, "O" * 100_000)

asyncio.run(main())

实际上,使用threadingasyncio是非常常见的,以至于在Python 3.9中,他们添加了asyncio.to_thread(func,*args,**kwargs)来缩短默认ThreadPoolExecutor的长度。

长答案


这种方法有没有不足之处?

有。对于asyncio,最大的缺点是异步函数与同步函数不同。这可能会让asyncio的新用户遇到很多问题,并导致需要做很多重新工作,如果你一开始没有考虑使用asyncio进行编程。

另一个缺点是你的代码用户也将被迫使用asyncio。所有这些必要的重新工作通常会让第一次使用asyncio的用户感到非常不愉快。

这种方法有没有非性能优势?

有。类似于使用concurrent.futures相对于threading.Threadmultiprocessing.Process具有统一接口的优势,这种方法可以被视为从执行程序到异步函数的进一步抽象。你可以从使用asyncio开始,如果后来发现你需要部分使用threadingmultiprocessing,你可以使用asyncio.to_threadrun_in_executor. 同样,你可能会发现已经存在一个异步版本的你正在尝试使用线程运行的东西,因此你可以轻松地停止使用threading并改用asyncio

这种方法有没有性能优势?

有...也有没有。最终取决于任务。在某些情况下,它可能不会有所帮助(尽管它可能不会有害),而在其他情况下,它可能会有很大的帮助。本答案的其余部分提供了一些关于使用asyncio运行Executor的原因的解释。

- 结合多个执行程序和其他异步代码

asyncio本质上提供了更多的控制并发性的代价是需要更多地掌控并发性。如果您想同时运行一些使用ThreadPoolExecutor的代码以及同时运行一些使用ProcessPoolExecutor的其他代码,使用同步代码管理这些代码并不容易,但使用asyncio非常容易。

import asyncio
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor

async def with_processing():
    with ProcessPoolExecutor() as executor:
        tasks = [...]
        for task in asyncio.as_completed(tasks):
            result = await task
            ...

async def with_threading():
    with ThreadPoolExecutor() as executor:
        tasks = [...]
        for task in asyncio.as_completed(tasks):
            result = await task
            ...

async def main():
    await asyncio.gather(with_processing(), with_threading())

asyncio.run(main())
这个是如何工作的? 实际上,asyncio 会要求执行器运行它们的函数。然后,在执行器正在运行时,asyncio 会去运行其他代码。例如,ProcessPoolExecutor 启动了一堆进程,在等待这些进程完成时,ThreadPoolExecutor 启动了一堆线程。asyncio 然后会检查这些执行器,并在它们完成时收集它们的结果。此外,如果您有其他使用 asyncio 的代码,您可以在等待进程和线程完成时运行它们。

- 缩小需要执行器的代码部分

通常情况下,在您的代码中不会有很多执行器,但我看到人们在使用线程/进程时经常遇到的问题是,他们会将整个代码都塞到一个线程/进程中,期望它能正常工作。例如,我曾经看到过以下代码(大约):
from concurrent.futures import ThreadPoolExecutor
import requests

def get_data(url):
    return requests.get(url).json()["data"]

urls = [...]

with ThreadPoolExecutor() as executor:
    for data in executor.map(get_data, urls):
        print(data)

这段代码有趣的地方在于它在并发情况下比非并发情况下更慢。为什么?因为生成的 json 很大,而让许多线程消耗大量内存是灾难性的。幸运的是,解决方案很简单:
from concurrent.futures import ThreadPoolExecutor
import requests

urls = [...]

with ThreadPoolExecutor() as executor:
    for response in executor.map(requests.get, urls):
        print(response.json()["data"])

现在一次只有一个json被加载到内存中,一切都很好。
这里的教训是什么呢?
引用:
你不应该试图将所有代码都拍成线程/进程,而应该集中精力处理实际需要并发的代码部分。
但是,如果get_data不像这个例子中的简单函数那样简单呢?如果我们必须在函数中间的某个深处应用执行器呢?这就是asyncio发挥作用的地方:
import asyncio
import requests

async def get_data(url):
    # A lot of code.
    ...
    # The specific part that needs threading.
    response = await asyncio.to_thread(requests.get, url, some_other_params)
    # A lot of code.
    ...
    return data

urls = [...]

async def main():
    tasks = [get_data(url) for url in urls]
    for task in asyncio.as_completed(tasks):
        data = await task
        print(data)

asyncio.run(main())

使用 concurrent.futures 进行同样的操作并不是很简单。你可以使用回调函数、队列等方式,但相比基本的 asyncio 代码,它将更难管理。


你能详细说明为什么使用requests.get而不是get_data可以避免将JSON对象卸载到内存中吗?它们都是函数,为了从中返回,requests.get似乎也需要将对象卸载到内存中。 - Zac Wrangler
2
@ZacWrangler 这里有两个重要的组成部分:requests.get(...).json()["data"]。其中一个执行API请求,另一个将所需数据加载到内存中。将 threading 应用于 API 请求可能会显著提高性能,因为您的计算机不需要为其执行任何工作,只需等待下载完成即可。将 threading 应用于 .json()["data"] 可能(并且很可能)导致多个 .json() 同时启动,最终跟随 ["data"],也许在所有 .json() 都运行之后。 - Simply Beautiful Art
3
在后一种情况下,这可能会导致大量的内存一次性加载(.json()的大小乘以线程数),这对性能来说是灾难性的。使用asyncio,您可以轻松地选择使用threading运行哪些代码和不运行哪些代码,从而使您能够选择不使用threading运行.json()["data"],而是只一次加载一个。 - Simply Beautiful Art
非常感谢。基于您的经验,是否有比Asyncio EventLoop更快或更好地处理Python线程的方法呢? - Avv
1
@Avv 就事件循环本身而言,从事件循环本身减速应该是相当困难的。换句话说,事件循环很可能不是问题所在,而是你编写的其他代码存在问题。使用 asyncio 的主要优势在于能够清晰地组织代码,提供更多避免编写不良并发代码的方式。 - Simply Beautiful Art

9

已经有很多好的答案了,关于何时使用每个答案无法再详细阐述。这是两者的有趣组合。Multiprocessing + asyncio: https://pypi.org/project/aiomultiprocess/.

它被设计用于高IO的用例,但仍然利用尽可能多的可用核心。Facebook使用这个库编写了一些基于Python的文件服务器。Asyncio允许IO限制流量,而multiprocessing允许在多个核心上使用多个事件循环和线程。

来自存储库的示例代码:

import asyncio
from aiohttp import request
from aiomultiprocess import Pool

async def get(url):
    async with request("GET", url) as response:
        return await response.text("utf-8")

async def main():
    urls = ["https://jreese.sh", ...]
    async with Pool() as pool:
        async for result in pool.map(get, urls):
            ...  # process result
            
if __name__ == '__main__':
    # Python 3.7
    asyncio.run(main())
    
    # Python 3.6
    # loop = asyncio.get_event_loop()
    # loop.run_until_complete(main())

这里只是一个补充说明,如果在例如Jupyter Notebook中使用,则无法正常工作,因为该Notebook已经有一个异步事件循环在运行。这只是一个小提示,以免让您感到恼火。


这并不需要整个包,你可以看看我的答案,了解如何使用普通的asyncioconcurrent.futures.ProcessPoolExecutor来完成大部分工作。值得注意的是,aiomultiprocessing在协程上运行,这意味着它可能会生成许多事件循环,而不是使用一个统一的事件循环(从源代码中可以看出),好坏参半。 - Simply Beautiful Art
当然,这并不是一个库所必需的。但是,该库的重点在于多个事件循环。这是在 Facebook 构建的,在那里他们想要为基于 Python 的对象/文件存储使用每个可用的 CPU。想象一下 Django 使用 uwsgi 生成多个子进程,并且每个子进程都有多个线程。 - Christo Goosen
此外,这个库还会移除一些样板代码,使开发人员的工作更加简单。 - Christo Goosen
1
感谢您解释了这个区别,我现在对它的目的有了更好的理解。与通常认为的用于计算密集型任务的 multiprocessing 不同,它真正发挥作用的地方是运行多个事件循环。也就是说,如果您发现 asyncio 的事件循环本身已经成为瓶颈,例如由于服务器上客户端数量过多,那么这就是要选择的选项。 - Simply Beautiful Art
很高兴。是的,我刚好看了一个YouTube视频,作者在其中描述了它的使用方式。这非常有见地,因为它很好地解释了其目的。它绝对不是万能药,也可能并非所有人都适用。也许会是Web服务器或低级网络应用程序的核心。基本上只需处理尽可能多的请求,CPU和多个事件循环可以处理。https://www.youtube.com/watch?v=0kXaLh8Fz3k - Christo Goosen

8

我不是专业的Python用户,但作为一名计算机体系结构的学生,我认为我可以在选择多处理和多线程之间分享我的一些考虑。此外,一些其他答案(即使是那些得票较高的答案)滥用了技术术语,因此我认为有必要首先对它们进行一些澄清。

多进程和多线程之间的根本区别在于它们是否共享同一内存空间。线程共享访问同一虚拟内存空间,因此线程之间交换计算结果非常高效和简单(零拷贝,并且完全在用户空间执行)。

另一方面,进程拥有独立的虚拟内存空间。它们无法直接读取或写入其他进程的内存空间,就像一个人不能在没有与他交谈的情况下阅读或更改另一个人的思想一样。(这样做将违反内存保护并且会破坏使用虚拟内存的目的。)为了在进程之间交换数据,它们必须依赖于操作系统的功能(例如消息传递),出于多种原因,这比线程使用的“共享内存”方案更加昂贵。其中一个原因是调用操作系统的消息传递机制需要进行系统调用,这将使代码执行从用户模式切换到内核模式,这需要耗费时间;另一个原因可能是操作系统的消息传递方案需要将数据字节从发送者的内存空间复制到接收者的内存空间,所以存在非零复制成本。
说一个多线程程序只能使用一个CPU是不正确的。许多人这样说的原因是CPython实现的副产品:全局解释器锁(GIL)。由于GIL,CPython进程中的线程被串行化。因此,看起来多线程Python程序只使用一个CPU。
但是,一般情况下,多线程计算机程序不限于一个核心。对于Python来说,如果不使用GIL的实现,确实可以同时运行多个线程,即在多个CPU上同时运行(请参见https://wiki.python.org/moin/GlobalInterpreterLock)。
鉴于CPython是Python主要的实现,理解为什么多线程Python程序通常被视为绑定到单个核心是可以理解的。
对于带有GIL的Python,释放多核心的能力的唯一方法是使用多进程(有例外情况如下所述)。但是,您的问题最好易于分区为具有最小互联的并行子问题,否则将需要进行大量的进程间通信。正如上面所解释的那样,使用操作系统的消息传递机制的开销将是昂贵的,有时甚至会完全抵消并行处理的好处。如果您的问题性质需要并发例程之间的密集通信,则多线程是自然的选择。不幸的是,在CPython中,真正有效的并行多线程是不可能的,因为存在GIL。在这种情况下,您应该意识到Python不是您的项目的最佳工具,并考虑使用另一种语言。
有一个替代方案,就是在C(或其他语言)中编写并实现并发处理例程的外部库,并将该模块导入Python。 CPython GIL不会阻止由该外部库生成的线程。
那么,在GIL的负担下,CPython的多线程还有用吗?尽管如其他答案所述,它仍然具有优点,如果您正在进行IO或网络通信。在这些情况下,相关计算不是由CPU完成的,而是由其他设备完成的(在IO的情况下,磁盘控制器和DMA(直接内存访问)控制器将以最小的CPU参与传输数据;在网络的情况下,NIC(网络接口卡)和DMA将负责大部分任务而无需CPU参与),因此一旦线程将这样的任务委托给NIC或磁盘控制器,操作系统可以将该线程置于睡眠状态并切换到同一程序的其他线程以执行有用的工作。
据我理解,asyncio模块本质上是IO操作的多线程的特定情况。
所以: CPU密集型程序,可以轻松地分区运行在具有有限通信的多个进程上:如果没有GIL(例如Jython),请使用多线程,否则请使用多进程(例如CPython)。

需要大量CPU计算且需要并发程序间的密集通信:如果GIL不存在,请使用多线程,或者使用另一种编程语言。

大量IO操作:使用asyncio。


7
  • 多进程可以并行运行。

  • 多线程asyncio无法并行运行。

在拥有Intel(R) Core(TM) i7-8700K CPU @ 3.70GHz32.0 GB RAM的计算机上,我用2个进程2个线程2个asyncio任务计时了从2100000之间的质数数量。下表展示了这个CPU密集型计算的结果:

多进程 多线程 asyncio
23.87 秒 45.24 秒 44.77 秒

多进程因为可以并行运行,所以比多线程asyncio快将近两倍,如上表所示。

我使用了以下3组代码:

多进程:

# "process_test.py"

from multiprocessing import Process
import time
start_time = time.time()

def test():
    num = 100000
    primes = 0
    for i in range(2, num + 1):
        for j in range(2, i):
            if i % j == 0:
                break
        else:
            primes += 1
    print(primes)

if __name__ == "__main__": # This is needed to run processes on Windows
    process_list = []

    for _ in range(0, 2): # 2 processes
        process = Process(target=test)
        process_list.append(process)

    for process in process_list:
        process.start()

    for process in process_list:
        process.join()

    print(round((time.time() - start_time), 2), "seconds") # 23.87 seconds

结果:

...
9592
9592
23.87 seconds

多线程:

# "thread_test.py"

from threading import Thread
import time
start_time = time.time()

def test():
    num = 100000
    primes = 0
    for i in range(2, num + 1):
        for j in range(2, i):
            if i % j == 0:
                break
        else:
            primes += 1
    print(primes)

thread_list = []

for _ in range(0, 2): # 2 threads
    thread = Thread(target=test)
    thread_list.append(thread)
    
for thread in thread_list:
    thread.start()

for thread in thread_list:
    thread.join()

print(round((time.time() - start_time), 2), "seconds") # 45.24 seconds

结果:

...
9592
9592
45.24 seconds

异步IO:

# "asyncio_test.py"

import asyncio
import time
start_time = time.time()

async def test():
    num = 100000
    primes = 0
    for i in range(2, num + 1):
        for j in range(2, i):
            if i % j == 0:
                break
        else:
            primes += 1
    print(primes)

async def call_tests():
    tasks = []

    for _ in range(0, 2): # 2 asyncio tasks
        tasks.append(test())

    await asyncio.gather(*tasks)

asyncio.run(call_tests())

print(round((time.time() - start_time), 2), "seconds") # 44.77 seconds

结果:

...
9592
9592
44.77 seconds

1

多进程 每个进程都有自己的Python解释器,并且可以在处理器的不同核心上运行。Python的multiprocessing是一个支持使用类似于线程模块的API来生成进程的包。multiprocessing包提供真正的并行性,通过使用子进程而不是线程有效地绕过全局解释器锁。

当您有CPU密集型任务时,请使用multiprocessing。

多线程 Python的多线程允许您在进程内生成多个线程。这些线程可以共享进程的相同内存和资源。在CPython中,由于全局解释器锁,在任何给定时间只能运行单个线程,因此无法利用多个核心。由于GIL的限制,Python中的多线程不提供真正的并行性。

Asyncio Asyncio基于协作式多任务概念工作。Asyncio任务在同一线程上运行,因此没有并行性,但它为开发人员提供了比多线程更好的控制,这是在多线程中的情况下操作系统提供的。

关于asyncio优于线程的优点,可以参考此链接进行讨论。

Lei Mao在Python并发性方面有一篇不错的博客在这里

Python中多进程VS多线程VS AsyncIO总结


0
只是为了在比较asynciomultithreading之间添加一个代码示例,因为我在这篇文章中没有看到一个:
这是一个使用asyncio运行的代码,输出是确定性的。
import asyncio


async def foo():
    print('Start foo()')
    for x in range(10):
        await asyncio.sleep(0.1)
        print(x, "foooo", x, "foooo",)
    print('End foo()')


async def bar():
    print('Start bar()')
    for x in range(10):
        await asyncio.sleep(0.1)
        print(x, "barrr", x, "barrr",)
    print('End bar()')


async def main():
    await asyncio.gather(foo(), bar())

asyncio.run(main())

输出:

Start foo()
Start bar()
0 foooo 0 foooo
0 barrr 0 barrr
1 foooo 1 foooo
1 barrr 1 barrr
2 foooo 2 foooo
2 barrr 2 barrr
3 foooo 3 foooo
3 barrr 3 barrr
4 foooo 4 foooo
4 barrr 4 barrr
5 foooo 5 foooo
5 barrr 5 barrr
6 foooo 6 foooo
6 barrr 6 barrr
7 foooo 7 foooo
7 barrr 7 barrr
8 foooo 8 foooo
8 barrr 8 barrr
9 foooo 9 foooo
End foo()
9 barrr 9 barrr
End bar()

与使用多线程运行的代码相比,输出不是确定性的,并且在每次运行时会发生变化。

import threading
import time


def foo():
    print('Start foo()')
    for x in range(10):
        time.sleep(0.1)
        print(x, "foooo", x, "foooo",)
    print('End foo()')


def bar():
    print('Start bar()')
    for x in range(10):
        time.sleep(0.1)
        print(x, "barrr", x, "barrr",)
    print('End bar()')


t1 = threading.Thread(target=foo)
t2 = threading.Thread(target=bar)

t1.start()
t2.start()

t1.join()
t2.join()

输出:

Start bar()Start foo()

0 0 foooo 0 foooo
barrr 0 barrr
11 foooo barrr  11  foooobarrr

22  foooobarrr  22 barrr 
foooo
3 3 barrr foooo3  3 foooobarrr

44 barrr 4  barrr
foooo 4 foooo
55  barrr foooo5  5barrr 
foooo
66  foooo 6 barrr foooo
6 barrr
7 7 foooo 7 foooo
barrr 7 barrr
88 foooo  8 foooo
barrr 8 barrr
99 foooo barrr  99  foooobarrr
End foo()

End bar()

在多线程中,上下文切换是自动发生的,而在asyncio中,上下文切换只会在await语句之后发生。
还要注意,在没有await asyncio.sleep(0.1)的asyncio示例中,代码的行为将像普通的同步代码一样,但在多线程示例中,即使没有time.sleep,代码仍将保持异步。

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