多处理与线程Python

我试图理解多重处理线程的优势。我知道多重处理绕过了全局解释器锁,但是还有什么其他优势,线程不能做同样的事情吗?

501583 次浏览

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

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

生成进程比生成线程慢一点。

另一件没有提到的事情是,它取决于你在速度方面使用的操作系统。在Windows中,进程成本很高,所以线程在Windows中会更好,但在unix中,进程比它们的windows变体更快,所以在unix中使用进程更安全,而且产卵更快。

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

请注意,由于GIL的原因,应用程序实际上并没有同时做两件事,但我们所做的是将数据库上的资源锁放入一个单独的线程中,以便CPU时间可以在它和用户交互之间切换。CPU时间在线程之间被定量分配。

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

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

多重处理

优点

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

缺点

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

线程

优点

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

缺点

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

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

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

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

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

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

然而,当你的任务是IO绑定时,多线程就变得有用了。例如,如果你的大部分任务都涉及等待API调用,你会使用多线程,因为为什么不在等待时在另一个线程中启动另一个请求,而不是让你的CPU无所事事。

太长别读

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

Python留档引号

这个答案的规范版本现在在duplquee问题:线程和多重处理模块有什么区别?

我已经强调了关于进程与线程和GIL的关键Python留档引号:什么是CPython中的全局解释器锁(GIL)?

进程vs线程实验

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

在基准测试中,我为8超线程 CPU上不同数量的线程计时CPU和IO绑定工作。每个线程提供的工作总是相同的,这样更多的线程意味着提供更多的总工作。

结果如下:

在此处输入图片描述

情节数据

结论:

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

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

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

    与C POSIX CPU绑定的工作相比,它达到了预期的8倍加速:“real”、“user”和“sys”在时间(1)的输出中是什么意思?

    待办事项:我不知道这是什么原因,一定有其他Python的低效率发挥作用。

测试代码:

#!/usr/bin/env python3
import multiprocessingimport threadingimport timeimport 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) % 10000000return result
class CpuThread(threading.Thread):def __init__(self, niters):super().__init__()self.niters = nitersself.result = 1def run(self):self.result = cpu_func(self.result, self.niters)
class CpuProcess(multiprocessing.Process):def __init__(self, niters):super().__init__()self.niters = nitersself.result = 1def run(self):self.result = cpu_func(self.result, self.niters)
class IoThread(threading.Thread):def __init__(self, sleep):super().__init__()self.sleep = sleepself.result = self.sleepdef run(self):time.sleep(self.sleep)
class IoProcess(multiprocessing.Process):def __init__(self, sleep):super().__init__()self.sleep = sleepself.result = self.sleepdef run(self):time.sleep(self.sleep)
if __name__ == '__main__':cpu_n_iters = int(sys.argv[1])sleep = 1cpu_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上游+在同一目录上绘制代码

在Ubuntu 18.10上测试,Python 3.6.7,在联想ThinkPadP51笔记本电脑上,CPU:英特尔酷睿i7-7820HQ CPU(4核/8线程),RAM:2x三星M471A2K43BB1-CRC(2x 16GiB),SSD:三星MZVLB512HAJQ-000L7(3,000 MB/s)。

可视化哪些线程在给定时间运行

这篇文章https://rohanvarma.me/GIL/告诉我,无论何时使用#0#1的参数调度线程,您都可以运行回调,multiprocessing.Process也是如此。

这使我们能够准确查看每次运行的线程。完成后,我们会看到类似(我制作了这个特定的图表):

            +--------------------------------------++ Active threads / processes           ++-----------+--------------------------------------+|Thread   1 |********     ************             ||         2 |        *****            *************|+-----------+--------------------------------------+|Process  1 |***  ************** ******  ****      ||         2 |** **** ****** ** ********* **********|+-----------+--------------------------------------++ Time -->                             ++--------------------------------------+

这将表明:

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

多重

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

多线程

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

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

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

它有线程池进程池

以下给出了一个见解:

线程池实例

import concurrent.futuresimport 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 contentsdef 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 promptlywith concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:# Start the load operations and mark each future with its URLfuture_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)))

进程池执行人

import concurrent.futuresimport 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 Falsereturn 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()

正如我在大学里学到的,上面的大多数答案都是对的。在不同平台(总是使用python)上的PRACTISE中,生成多个线程最终就像生成一个进程。不同的是,多核分担负载,而不是只有一个内核以100%的速度处理所有内容。所以,如果你在4核的电脑上生成10个线程,你最终将只获得25%的cpu能力!如果你生成10个进程,你最终将获得100%的cpu处理能力(如果你没有其他限制)。我不是所有新技术的专家。我用自己的真实经验背景回答

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

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

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

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

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

多处理的2个用例:在程序是CPU密集型并且不必进行任何IO或用户交互的情况下,多处理优于线程。

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