多处理与线程Python

Joh*_*ohn 739 python multithreading multiprocessing

我试图了解多处理优于线程的优势.我知道多处理可以解决Global Interpreter Lock问题,但是还有什么其他优点,并且线程不能做同样的事情?

Jer*_*own 779

以下是我想出的一些优点/缺点.

优点

  • 单独的内存空间
  • 代码通常很简单
  • 利用多个CPU和核心
  • 避免cPython的GIL限制
  • 消除对同步原语的大多数需求,除非您使用共享内存(相反,它更像是IPC的通信模型)
  • 子进程是可中断/可杀死的
  • Python multiprocessing模块包含有用的抽象,界面很像threading.Thread
  • 必须使用cPython进行CPU绑定处理

缺点

  • IPC更复杂,开销更大(通信模型与共享内存/对象)
  • 更大的内存占用

穿线

优点

  • 轻量级 - 内存占用少
  • 共享内存 - 更容易从另一个上下文访问状态
  • 允许您轻松制作响应式用户界面
  • 正确释放GIL的cPython C扩展模块将并行运行
  • I/O绑定应用程序的绝佳选择

缺点

  • cPython - 受GIL限制
  • 不可中断/可杀
  • 如果不遵循命令队列/消息泵模型(使用Queue模块),则手动使用同步原语成为必需(锁定粒度需要决策)
  • 代码通常难以理解并且正确 - 竞争条件的可能性会急剧增加

  • @Deqing不,不.在Python中,由于GIL(全局解释器锁),单个python进程无法并行运行线程(使用多个核心).然而,它可以同时运行它们(在I/O绑定操作期间进行上下文切换). (71认同)
  • 对于多进程:"利用多个CPU和核心".线程也有这个专业吗? (32认同)
  • @camconn"@AndrewGuenther直接来自**多处理**文档"是的,**多处理**包可以做到这一点,但不是**多线程**包,这是我的评论所指的. (25认同)
  • @AndrewGuenther直接来自多处理文档(强调我的):"多处理包提供本地和远程并发,**通过使用子进程而不是线程有效地侧面执行全局解释器锁**.因此,多处理模块允许程序员在给定的机器上充分利用**多个处理器**." (9认同)
  • @AndrewGuenther Mea copa.我是一个尝试聪明的ID10T.我的错. (9认同)
  • 你能举一个“正确释放GIL的cPython C扩展模块将并行运行”的例子吗? (3认同)
  • 多处理con:`logging`库与它不兼容. (3认同)
  • 在空闲列表的情况下,多重处理可能会[产生更小的内存占用](http://stackoverflow.com/a/1316799/1658908)。 (2认同)
  • @camconn我想您想说“ Mea culpa”,这将是“我的错”。“ Mea Copa”就像是拉丁文和西班牙文的混合物,说“ My cup”。 (2认同)

Sjo*_*erd 653

threading模块使用线程,该multiprocessing模块使用进程.不同之处在于线程在相同的内存空间中运行,而进程具有单独的内存.这使得在具有多处理的进程之间共享对象变得有点困难.由于线程使用相同的内存,因此必须采取预防措施,否则两个线程将同时写入同一内​​存.这就是全局解释器锁的用途.

产卵过程比产生线程慢一点.一旦它们运行,就没有太大区别.

  • cPython*中的GIL不会*保护您的程序状态.它保护翻译的状态. (172认同)
  • 此外,OS处理进程调度.线程库处理线程调度.并且,线程共享I/O调度 - 这可能是一个瓶颈.进程具有独立的I/O调度. (38认同)
  • 实际上存在很大差异:http://eli.thegreenplace.net/2012/01/16/python-parallelizing-cpu-bound-tasks-with-multiprocessing/ (16认同)
  • 多处理的IPC性能怎么样?对于需要在进程间频繁共享对象的程序(例如,通过multiprocessing.Queue),与进程中队列的性能比较是什么? (3认同)
  • 如果由于CPU可能耗尽进程/内存而过于频繁地生成进程,是否存在问题.但是,如果太多的线程经常产生,但仍然比多个进程的开销更小,它可以是相同的.对? (3认同)
  • “一旦他们开始跑步,就没有太大区别。” - 你确定吗?由于它们使用相同的内存空间,线程之间的通信可以比进程之间的通信快得多...... (2认同)
  • @S.Lott 你确定“Python 中的线程库处理线程调度吗?”。[David Beazley](http://www.dabeaz.com/python/GIL.pdf) 说“Python 没有线程调度程序 • 没有线程优先级、抢占、循环调度等概念。 • 所有线程调度留给主机操作系统(例如Linux、Windows等)”现在我很困惑哪一个是正确的? (2认同)
  • 这个答案有很多问题,但是对我来说,突出的是:“生成进程比生成线程要慢一些。一旦运行,它们之间就不会有太大的区别。”实际上,线程是贫血的。 python与“ Global Interpreter Lock”相比,与其他语言相比。由于该锁定,即使您有来自操作系统的多个线程,所有内容仍将与多线程同步处理。如果您正确地建立了并行性,那么使用多处理应该会获得显着的收益。 (2认同)

Sim*_*bbs 200

线程的工作是使应用程序能够响应.假设您有数据库连接,并且需要响应用户输入.如果没有线程,如果数据库连接繁忙,应用程序将无法响应用户.通过将数据库连接拆分为单独的线程,可以使应用程序更具响应性.此外,由于两个线程都在同一个进程中,因此它们可以访问相同的数据结构 - 良好的性能以及灵活的软件设计.

请注意,由于GIL,应用程序实际上并没有同时执行两项操作,但我们所做的是将数据库上的资源锁定放入一个单独的线程中,以便可以在它与用户交互之间切换CPU时间.CPU时间在线程之间得到限制.

多处理是指您确实希望在任何给定时间完成多项操作的时间.假设您的应用程序需要连接到6个数据库并对每个数据集执行复杂的矩阵转换.将每个作业放在一个单独的线程中可能会有所帮助,因为当一个连接空闲时,另一个可能会获得一些CPU时间,但是处理不会并行完成,因为GIL意味着您只使用一个CPU的资源.通过将每个作业置于多处理过程中,每个作业都可以在其自己的CPU上运行并以最高效率运行.

  • @NishantKashyap - 重读你从中引用的句子.Simon谈论多线程的处理 - 它不是关于多处理. (6认同)
  • “但是处理不会并行完成,因为 GIL 意味着你只使用一个 CPU 的资源”多处理中的 GIL 怎么会......? (2认同)

Mar*_*tos 40

关键优势是隔离.崩溃进程不会导致其他进程崩溃,而崩溃的进程可能会对其他线程造成严重破坏.

  • @ArtOfWarfare线程可以做的不仅仅是引发异常.流氓线程可以通过bug本机或ctypes代码在进程中的任何地方废弃内存结构,包括python运行时本身,从而破坏整个进程. (6认同)
  • 很确定这是错的.如果Python中的标准线程通过引发异常而结束,则在您加入它时不会发生任何事情.我编写了自己的线程子类,它捕获线程中的异常并在连接它的线程上重新引发它,因为它只是忽略的事实非常糟糕(导致其他很难找到错误.)一个进程会有相同的行为.除非崩溃,否则意味着Python实际崩溃,而不是引发异常.如果您发现Python崩溃,那肯定是您应该报告的错误.Python应该始终引发异常而不会崩溃. (4认同)

chr*_*ley 27

另一件未提及的事情是它取决于您在速度方面使用的操作系统.在Windows中,进程成本很高,因此在Windows中线程会更好,但是在unix进程中它们比它们的windows变体更快,因此在unix中使用进程更加安全,而且可以快速生成.

  • 你有实际的数字支持吗?IE,比较连续执行任务,然后在多个线程上,然后在Windows和Unix上的多个进程上进行比较? (5认同)
  • 同意@ArtOfWarfare问题.数字?您是否建议使用Threads for Windows? (3认同)

Cir*_*四事件 24

Python文档引号

我在以下位置突出了有关Process vs Threads和GIL的主要Python文档报价:CPython中的全局解释器锁(GIL)是什么?

进程与线程实验

我做了一些基准测试,以便更具体地显示差异。

在基准测试中,我为8个超线程 CPU 上的CPU和IO绑定工作定时了各种数量的线程。每个线程提供的功总是相同的,因此,更多线程意味着提供更多的总功。

结果是:

在此处输入图片说明

绘制数据

结论:

  • 对于CPU限制的工作,多处理总是更快,大概是由于GIL

  • 用于IO绑定工作。两者的速度完全一样

  • 由于我在8个超线程计算机上,因此线程最多只能扩展到大约4倍,而不是预期的8倍。

    与C POSIX绑定的CPU工作达到预期的8倍加速的情况相反:“ real”,“ user”和“ sys”在time(1)的输出中意味着什么?

    TODO:我不知道原因,一定还有其他Python效率低下。

测试代码:

#!/usr/bin/env python3

import multiprocessing
import threading
import time
import sys

def cpu_func(result, niters):
    '''
    A useless CPU bound function.
    '''
    for i in range(niters):
        result = (result * result * i + 2 * result * i * i + 3) % 10000000
    return result

class CpuThread(threading.Thread):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class CpuProcess(multiprocessing.Process):
    def __init__(self, niters):
        super().__init__()
        self.niters = niters
        self.result = 1
    def run(self):
        self.result = cpu_func(self.result, self.niters)

class IoThread(threading.Thread):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

class IoProcess(multiprocessing.Process):
    def __init__(self, sleep):
        super().__init__()
        self.sleep = sleep
        self.result = self.sleep
    def run(self):
        time.sleep(self.sleep)

if __name__ == '__main__':
    cpu_n_iters = int(sys.argv[1])
    sleep = 1
    cpu_count = multiprocessing.cpu_count()
    input_params = [
        (CpuThread, cpu_n_iters),
        (CpuProcess, cpu_n_iters),
        (IoThread, sleep),
        (IoProcess, sleep),
    ]
    header = ['nthreads']
    for thread_class, _ in input_params:
        header.append(thread_class.__name__)
    print(' '.join(header))
    for nthreads in range(1, 2 * cpu_count):
        results = [nthreads]
        for thread_class, work_size in input_params:
            start_time = time.time()
            threads = []
            for i in range(nthreads):
                thread = thread_class(work_size)
                threads.append(thread)
                thread.start()
            for i, thread in enumerate(threads):
                thread.join()
            results.append(time.time() - start_time)
        print(' '.join('{:.6e}'.format(result) for result in results))
Run Code Online (Sandbox Code Playgroud)

GitHub上游+在同一目录上绘制代码

在带有CPU的Lenovo ThinkPad P51笔记本电脑上的Ubuntu 18.10,Python 3.6.7上进行了测试:Intel Core i7-7820HQ CPU(4核/ 8线程),RAM:2x三星M471A2K43BB1-CRC(2x 16GiB),SSD:三星MZVLB512HAJQ- 000L7(3,000 MB / s)。

可视化在给定时间正在运行的线程

这篇文章https://rohanvarma.me/GIL/告诉我,每当一个线程被调度与您可以运行一个回调target=的参数threading.Thread和相同的multiprocessing.Process

这使我们可以精确查看每次运行哪个线程。完成此操作后,我们将看到类似的内容(我制作了此特定图形):

            +--------------------------------------+
            + Active threads / processes           +
+-----------+--------------------------------------+
|Thread   1 |********     ************             |
|         2 |        *****            *************|
+-----------+--------------------------------------+
|Process  1 |***  ************** ******  ****      |
|         2 |** **** ****** ** ********* **********|
+-----------+--------------------------------------+
            + Time -->                             +
            +--------------------------------------+
Run Code Online (Sandbox Code Playgroud)

这将表明:

  • 线程由GIL完全序列化
  • 进程可以并行运行

  • @AndrasDeak我将把它留在这里,因为否则这个页面会不太好,某些链接会损坏,我会失去来之不易的代表。 (4认同)

Chi*_*rav 18

其他答案更多地关注多线程与多处理方面,但在python 中必须考虑全局解释器锁(GIL).当创建更多数量(比如k)的线程时,通常它们不会将性能提高k倍,因为它仍将作为单线程应用程序运行.GIL是一个全局锁,可以锁定所有内容,并且只允许单个线程执行,只使用一个内核.在使用Numpy,Network,I/O等C扩展的地方,性能确实会增加,在那里完成了大量后台工作并发布了GIL.
因此,当使用线程时,只有一个操作系统级线程,而python创建伪线程,这些线程完全由线程本身管理,但基本上作为单个进程运行.抢占发生在这些伪线程之间.如果CPU以最大容量运行,您可能希望切换到多处理.
现在,如果是自包含的执行实例,您可以改为选择池.但是,如果数据重叠,您可能希望进程通信,您应该使用multiprocessing.Process.


Bol*_*boa 17

正如问题中所提到的,Python中的多处理是实现真正并行性的唯一真正方法.多线程无法实现这一点,因为GIL会阻止线程并行运行.

因此,螺纹可能不总是在Python有用的,而事实上,这取决于你想达到什么样的甚至可能导致更差的性能.例如,如果您正在执行CPU绑定任务,例如解压缩gzip文件或3D渲染(任何CPU密集型),那么线程实际上可能会妨碍您的性能而不是帮助.在这种情况下,您可能希望使用多处理,因为此方法实际上并行运行,并有助于分配手头任务的权重.可能会有一些开销因为多处理涉及将脚本的内存复制到每个子进程中,这可能会导致更大的应用程序出现问题.

但是,当您的任务受IO限制时,多线程会很有用.例如,如果您的大部分任务都涉及等待API调用,那么您将使用多线程,因为为什么不在等待时在另一个线程中启动另一个请求,而不是让您的CPU闲置.

TL; DR

  • 多线程是并发的,用于IO绑定任务
  • 多处理实现了真正的并行性,并用于CPU绑定任务

  • 你能举一个 IO 密集型任务的例子吗? (2认同)
  • @YellowPillow假设您要进行多个API调用以请求一些数据,在这种情况下,大部分时间都花在等待网络上。在等待该网络“ I / O”时,可以释放“ GIL”以供下一个任务使用。但是,该任务将需要重新获取`GIL`才能执行与每个API请求关联的所有python代码的其余部分,但是,由于该任务正在等待网络,因此无需继续执行到`GIL`。 (2认同)

Jer*_*ril 12

多处理

  • 多处理添加 CPU 以提高计算能力。
  • 多个进程同时执行。
  • 流程的创建是耗时且资源密集的。
  • 多处理可以是对称的或非对称的。
  • Python 中的多处理库使用单独的内存空间、多个 CPU 内核、绕过 CPython 中的 GIL 限制、子进程是可杀死的(例如程序中的函数调用)并且更易于使用。
  • 该模块的一些注意事项是更大的内存占用,并且 IPC 稍微复杂一些,开销更大。

多线程

  • 多线程创建单个进程的多个线程以增加计算能力。
  • 单个进程的多个线程并发执行。
  • 创建线程在时间和资源上都是经济的。
  • 多线程库是轻量级的,共享内存,负责响应式 UI,适用于 I/O 绑定应用程序。
  • 该模块不可杀死并且受 GIL 约束。
  • 多个线程生活在同一个空间的同一个进程中,每个线程会做一个特定的任务,有自己的代码,自己的栈内存,指令指针,共享堆内存。
  • 如果一个线程有内存泄漏,它可能会损坏其他线程和父进程。

使用 Python 的多线程和多处理示例

Python 3 具有启动并行任务的功能。这使我们的工作更轻松。

它有线程池进程池

下面给出了一个见解:

线程池执行器示例

import concurrent.futures
import urllib.request

URLS = ['http://www.foxnews.com/',
        'http://www.cnn.com/',
        'http://europe.wsj.com/',
        'http://www.bbc.co.uk/',
        'http://some-made-up-domain.com/']

# Retrieve a single page and report the URL and contents
def load_url(url, timeout):
    with urllib.request.urlopen(url, timeout=timeout) as conn:
        return conn.read()

# We can use a with statement to ensure threads are cleaned up promptly
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    # Start the load operations and mark each future with its URL
    future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
    for future in concurrent.futures.as_completed(future_to_url):
        url = future_to_url[future]
        try:
            data = future.result()
        except Exception as exc:
            print('%r generated an exception: %s' % (url, exc))
        else:
            print('%r page is %d bytes' % (url, len(data)))
Run Code Online (Sandbox Code Playgroud)

进程池执行器

import concurrent.futures
import math

PRIMES = [
    112272535095293,
    112582705942171,
    112272535095293,
    115280095190773,
    115797848077099,
    1099726899285419]

def is_prime(n):
    if n % 2 == 0:
        return False

    sqrt_n = int(math.floor(math.sqrt(n)))
    for i in range(3, sqrt_n + 1, 2):
        if n % i == 0:
            return False
    return True

def main():
    with concurrent.futures.ProcessPoolExecutor() as executor:
        for number, prime in zip(PRIMES, executor.map(is_prime, PRIMES)):
            print('%d is prime: %s' % (number, prime))

if __name__ == '__main__':
    main()
Run Code Online (Sandbox Code Playgroud)


EL *_*BIR 9

线程共享相同的内存空间以保证两个线程不共享相同的内存位置,因此必须采取特殊的预防措施,CPython 解释器使用称为GIL全局解释器锁的机制处理此问题

什么是 GIL(只是我想澄清上面重复的GIL)?

在 CPython 中,全局解释器锁或 GIL 是一个互斥锁,用于保护对 Python 对象的访问,防止多个线程同时执行 Python 字节码。这个锁是必要的,主要是因为 CPython 的内存管理不是线程安全的。

对于主要问题,我们可以使用用例进行比较,如何?

1-Use Cases for Threading:在GUI程序的情况下,线程可用于使应用程序响应例如,在文本编辑程序中,一个线程可以负责记录用户输入,另一个可以负责显示文本,第三个可以进行拼写检查,等等。在这里,程序必须等待用户交互。这是最大的瓶颈。线程的另一个用例是 IO 绑定或网络绑定的程序,例如网络爬虫。

多处理的 2 用例:在程序占用大量 CPU 且无需执行任何 IO 或用户交互的情况下,多处理优于线程。

有关更多详细信息,请访问此链接链接,或者您需要深入了解线程访问此处进行多处理访问此处