多处理vs多线程vs asyncio

我发现在Python 3.4中有几个不同的多处理/线程库:多处理 vs 线程 vs asyncio

但我不知道该用哪一个,还是“推荐的一个”。它们做的事情是一样的,还是不同的?如果是的话,哪个是用来干什么的?我想在我的计算机上写一个使用多核的程序。但我不知道该学哪个图书馆。

101578 次浏览

它们用于(稍微)不同的目的和/或需求。CPython(典型的主线Python实现)仍然具有全局解释器锁,因此多线程应用程序(目前实现并行处理的标准方式)是次优的。这就是为什么multiprocessing 五月优先于threading。但是,并不是每个问题都可以有效地分割成[几乎独立的]部分,因此可能需要大量的进程间通信。这就是为什么一般情况下multiprocessing可能不优先于threading

asyncio(该技术不仅在Python中可用,其他语言和/或框架也有,例如提振。ASIO)是一种有效处理来自多个同步源的大量I/O操作的方法,而不需要并行代码执行。因此,它只是针对特定任务的解决方案(确实是一个不错的解决方案!),而不是用于一般的并行处理。

博士TL;

做出正确的选择:

我们已经介绍了最流行的并发形式。但问题依然存在——什么时候应该选择哪一个?这实际上取决于用例。根据我的经验(和阅读),我倾向于遵循以下伪代码:

if io_bound:
if io_very_slow:
print("Use Asyncio")
else:
print("Use Threads")
else:
print("Multi Processing")
  • CPU上限=>多处理
  • I/O绑定,快速I/O,有限连接数=>多的线程
  • I/O约束,慢I/O,多连接=>Asyncio

< a href = " http://masnun。岩石/ 2016/10/06 / async-python-the-different-forms-of-concurrency / noreferrer“rel = >引用< / >


[请注意]:

  • 如果你有一个很长的调用方法(例如,一个包含睡眠时间或惰性I/O的方法),最好的选择是asyncio扭曲的龙卷风方法(协程方法),它与单线程并发工作。
  • asyncio适用于Python3.4及以后版本。
  • 龙卷风扭曲的Python2.7开始准备
  • uvloop是超快的asyncio事件循环(uvloop使asyncio快2-4倍)。

(更新(2019)):

  • Japranto (GitHub)是一个基于uvloop的非常快速的流水线HTTP服务器。

这是基本思想:

它是IO-BOUND吗?-----------& gt;使用asyncio

是__abc1-heavy吗?---------& gt;使用multiprocessing

其他的吗?----------------------& gt;使用threading

所以基本上坚持线程,除非你有IO/CPU问题。

多处理中,你利用多个cpu来分布你的计算。由于每个cpu都是并行运行的,因此可以有效地同时运行多个任务。你会想要对中央处理器受限任务使用多处理。一个例子是试图计算一个庞大列表中所有元素的和。如果你的机器有8核,你可以“切割”;将列表分成8个更小的列表,在不同的核心上分别计算每个列表的和,然后将这些数字相加。这样做你会得到8倍的加速。

在(multi)线程中,你不需要多个cpu。想象一个向网络发送大量HTTP请求的程序。如果您使用单线程程序,它将在每个请求时停止执行(块),等待响应,然后在收到响应后继续执行。这里的问题是,你的CPU并没有真正工作,而等待一些外部服务器来做这项工作;在此期间,它实际上可以做一些有用的工作!解决办法是使用线程——你可以创建很多线程,每个线程负责从web请求一些内容。线程的好处是,即使它们运行在一个CPU上,CPU也会不时地“冻结”。执行一个线程并跳转到执行另一个线程(这称为上下文切换,它以不确定的间隔不断发生)。所以如果你的任务是I / O绑定 -使用线程。

asyncio本质上是线程,其中不是CPU,而是你,作为一个程序员(或者实际上是你的应用程序),决定上下文切换发生的时间和地点。在Python中,使用await关键字挂起协程的执行(使用async关键字定义)。

已经有很多好的答案了。无法详细说明何时使用每种方法。这更像是两者的有趣结合。Multiprocessing + asyncio: https://pypi.org/project/aiomultiprocess/

它的设计用例是高容量的,但仍然使用尽可能多的可用内核。Facebook使用这个库来编写某种基于python的文件服务器。Asyncio允许IO绑定流量,但multiprocessing允许多个事件循环和多个内核上的线程。

回购中的Ex代码:

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笔记本很好,因为笔记本已经有一个asyncio循环运行。只是给你留个小纸条,别扯头发。

许多答案建议如何只选择一个选项,但为什么不能使用所有三个选项呢?在这个回答中,我将解释如何使用asyncio来管理所有三种形式的并发如果需要,可以轻松地在它们之间交换的组合。

简短的回答


许多第一次接触Python并发性的开发人员最终会使用processing.Processthreading.Thread。然而,这些是由concurrent.futures模块提供的高级API合并在一起的低级API。此外,生成进程和线程有开销,比如需要更多内存,这个问题困扰着我下面展示的一个示例。在某种程度上,concurrent.futures为你管理这一点,这样你就不会轻易地做一些事情,比如生成一千个进程,只生成几个进程,然后在每次进程完成时重用这些进程,从而使你的计算机崩溃。

这些高级api是通过concurrent.futures.Executor提供的,然后由concurrent.futures.ProcessPoolExecutorconcurrent.futures.ThreadPoolExecutor实现。在大多数情况下,你应该使用它们而不是multiprocessing.Processthreading.Thread,因为将来当你使用concurrent.futures时,更容易从一个改变到另一个,而且你不必学习每个的详细差异。

由于它们共享一个统一的接口,你还会发现使用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,这种方法可以被视为从Executor到异步函数的进一步抽象。你可以开始使用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很大,而让许多线程消耗大量内存是灾难性的. c。幸运的是,解决方法很简单:

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代码更难管理。

  • 多处理可以运行平行

  • 多线程asyncio不能运行平行

使用英特尔(R)酷睿(TM) i7-8700K CPU @ 3.70GHz32.0 gb内存,我用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

Asyncio:

# "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

< >强多处理 每个进程都有自己的Python解释器,并且可以在处理器的独立内核上运行。Python multiprocessing是一个包,它支持使用类似于threading模块的API生成进程。多处理包提供了真正的并行性,通过使用子进程而不是线程,有效地避开了全局解释器锁

当你有CPU密集型任务时,使用多处理。

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

< >强Asyncio Asyncio致力于协作多任务概念。Asyncio任务运行在同一个线程上,因此没有并行性,但它为开发人员提供了更好的控制,而不是多线程中的操作系统

关于这个链接,关于asyncio相对于线程的优点有一个很好的讨论。

Multiprocessing VS Threading VS AsyncIO in Python Summary

我不是一个专业的Python用户,但作为一个计算机体系结构的学生,我想我可以分享一些我在多处理和多线程之间选择的考虑因素。此外,其他一些答案(甚至在那些投票较高的答案中)都是滥用技术术语,所以我认为也有必要对这些问题进行一些澄清,我将首先进行澄清。

多处理和多线程之间的根本区别在于它们是否共享相同的内存空间。线程共享对相同虚拟内存空间的访问,因此线程交换计算结果(零复制,完全用户空间执行)是高效且容易的。

另一方面,进程有独立的虚拟内存空间。它们不能直接读取或写入其他进程的内存空间,就像一个人不与另一个人交谈就不能读取或改变另一个人的思想一样。(允许这样做将违反内存保护,并违背使用虚拟内存的目的。)为了在进程之间交换数据,它们必须依赖于操作系统的功能(例如消息传递),而且出于多种原因,这样做比线程使用的“共享内存”方案成本更高。一个原因是调用OS的消息传递机制需要进行系统调用,将代码执行从用户模式切换到内核模式,这很耗时;另一个原因可能是OS消息传递方案必须将数据字节从发送方的内存空间复制到接收方的内存空间,因此复制成本非零。

说一个多线程程序只能使用一个CPU是不正确的。很多人这么说的原因是由于CPython实现的一个工件:全局解释器锁(GIL)。由于GIL的存在,CPython进程中的线程是序列化的。因此,多线程python程序似乎只使用了一个CPU。

但是多线程计算机程序通常不局限于一个核心,对于Python来说,不使用GIL的实现确实可以并行运行多个线程,也就是说,同时在多个CPU上运行。(见https://wiki.python.org/moin/GlobalInterpreterLock)。

考虑到CPython是Python的主要实现,可以理解为什么多线程Python程序通常等同于绑定到单核。

使用Python和GIL,释放多核功能的唯一方法是使用多处理(下面会提到一些例外情况)。但是你的问题最好能很容易地划分成具有最小相互通信的并行子问题,否则将不得不发生大量的进程间通信,正如上面所解释的,使用操作系统的消息传递机制的开销将是昂贵的,有时如此昂贵的并行处理的好处完全抵消了。如果您的问题本质上需要在并发例程之间进行频繁的通信,那么多线程是自然的选择。不幸的是,在CPython中,由于GIL,有效的并行多线程是不可能的。在这种情况下,您应该意识到Python不是项目的最佳工具,并考虑使用其他语言。

还有一种替代解决方案,即在用C(或其他语言)编写的外部库中实现并发处理例程,并将该模块导入Python。CPython GIL不会阻塞由该外部库生成的线程。

那么,考虑到GIL的负担,CPython中的多线程有什么好处吗?不过,正如其他答案所提到的,如果您正在进行IO或网络通信,它仍然有好处。在这些情况下,相关的计算不是由CPU完成的,而是由其他设备完成的(在IO的情况下,磁盘控制器和DMA(直接内存访问)控制器将在最小的CPU参与下传输数据;在联网的情况下,网卡(网络接口卡)和DMA将在没有CPU参与的情况下处理大部分任务),因此一旦一个线程将这样的任务委托给网卡或磁盘控制器,操作系统可以将该线程置于睡眠状态,并切换到同一程序的其他线程来做有用的工作。

在我的理解中,asyncio模块本质上是用于IO操作的多线程的特定情况。

< p >: cpu密集型程序,可以很容易地分区到多个进程上运行,但通信有限:如果GIL不存在,则使用多线程(如Jython),如果GIL存在,则使用多进程(如CPython)

cpu密集型程序,需要在并发例程之间进行密集的通信:如果GIL不存在,则使用多线程,或者使用其他编程语言。

大量的IO: asyncio