多处理与线程Python

时间:2010-06-15 11:12:46

标签: python multithreading multiprocessing

我想了解multiprocessing优于threading的优势。我知道多处理绕过Global Interpreter Lock,但还有其他哪些优点,线程可以做同样的事情吗?

11 个答案:

答案 0 :(得分:739)

以下是我提出的一些优点/缺点。

多处理

赞成

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

缺点

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

线程

赞成

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

缺点

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

答案 1 :(得分:599)

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

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

答案 2 :(得分:189)

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

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

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

答案 3 :(得分:39)

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

答案 4 :(得分:26)

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

答案 5 :(得分:17)

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

答案 6 :(得分:10)

正如问题中提到的那样,Python中的 Multiprocessing 是实现真正的并行性的唯一真正方法。 多线程无法实现此目的,因为 GIL 阻止线程并行运行。

结果,线程化在Python中可能并不总是有用,实际上,根据您要实现的目标,甚至可能导致性能下降。例如,如果您正在执行 CPU绑定任务,例如解压缩gzip文件或3D渲染(任何占用CPU的资源),那么线程实际上可能会阻碍性能而不是帮助。在这种情况下,您将要使用 Multiprocessing ,因为只有这种方法实际上可以并行运行,并且有助于分配手头任务的重量。由于 Multiprocessing 涉及将脚本的内存复制到每个子进程中,这可能会导致大型应用程序出现问题。

但是,当您的任务受 IO约束时,多线程会很有用。例如,如果您的大部分任务都涉及等待 API调用,那么您将使用 Multithreading ,因为为什么在等待时不启动另一个线程中的另一个请求,而不是您的CPU无所事事。

TL; DR

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

答案 7 :(得分:8)

Python文档报价

我在What is the global interpreter lock (GIL) in CPython?

处突出了有关Process vs Threads和GIL的主要Python文档报价。

进程与线程实验

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

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

结果是:

enter image description here

结论:

  • 对于CPU限制的工作,多处理总是更快,这可能是由于GIL造成的。

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

  • 线程只能扩展到大约4倍,而不是预期的8倍,因为我在8台超线程计算机上。

    与C POSIX绑定的工作达到了预期的8倍加速的对比:What do 'real', 'user' and 'sys' mean in the output of 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))

GitHub upstream + plotting code on same directory

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

答案 8 :(得分:2)

进程可能有多个线程。这些线程可以共享内存,并且是进程内的执行单元。

进程在CPU上运行,因此线程驻留在每个进程下。流程是独立运行的单个实体。如果要在每个流程之间共享数据或状态,可以使用内存存储工具,例如Cache(redis, memcache)FilesDatabase

答案 9 :(得分:1)

多处理

  • 多处理增加了CPU以提高计算能力。
  • 多个进程同时执行。
  • 创建流程既耗时又耗费资源。
  • 多重处理可以对称或不对称。
  
      
  • Python中的多处理库使用单独的内存空间,使用多个CPU内核,绕过CPython中的GIL限制,子进程是可终止的(例如程序中的函数调用)并且易于使用。
  •   
  • 该模块的一些警告是较大的内存占用,而IPC则稍微复杂一些,而且开销更多。
  •   

多重阅读

  • 多线程可在单个进程中创建多个线程,以提高计算能力。
  • 单个进程的多个线程同时执行。
  • 创建线程在时间和资源上都是经济的。
  
      
  • 多线程库是轻量级的,共享内存,负责响应UI,并且非常适合I / O绑定的应用程序。
  •   
  • 该模块不可杀,并且要遵守GIL。
  •   
  • 多个线程位于同一空间的同一进程中,每个线程将执行特定的任务,具有自己的代码,自己的堆栈内存,指令指针和共享堆内存。
  •   
  • 如果某个线程发生内存泄漏,则可能会损坏其他线程和父进程。
  •   

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

Python 3具有Launching parallel tasks的功能。这使我们的工作更加轻松。

它具有thread poolingProcess pooling的名称。

以下内容提供了一个见解:

ThreadPoolExecutor示例

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)))

ProcessPoolExecutor

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()

答案 10 :(得分:0)

在我上大学时,上面的大多数答案都是正确的。在不同平台上的PRACTICE(始终使用python)中,产生多个线程的结果类似于产生一个进程。区别在于多个内核共享负载,而不是只有1个内核以100%的速度处理所有内容。因此,如果您在4核PC上生成了10个线程,最终将只能获得25%的CPU功耗!而且,如果您生成10个进程,则最终将以100%的cpu处理(如果您没有其他限制)。我不是所有新技术的专家。我有自己的实际经验背景回答