线程和多处理模块之间有什么区别?

我正在学习如何使用 Python 中的 threadingmultiprocessing模块来并行运行某些操作并加快代码的速度。

我发现这很难(可能是因为我没有任何理论背景)去理解 threading.Thread()对象和 multiprocessing.Process()对象之间的区别。

另外,我不完全清楚如何实例化一个作业队列,并且只有4个(例如)作业并行运行,而另一个作业在执行之前等待资源释放。

我发现文档中的示例很清楚,但并不是非常详尽; 一旦我试图将事情复杂化一点,就会收到许多奇怪的错误(比如一个无法腌制的方法,等等)。

那么,我应该什么时候使用 threadingmultiprocessing模块呢?

您能给我提供一些资源,解释这两个模块背后的概念以及如何在复杂任务中正确使用它们吗?

78833 次浏览
一个进程中可以存在多个线程。 属于同一进程的线程共享相同的内存区域(可以从相同的变量读取和写入,并且可以相互干扰)。 相反,不同的进程位于不同的内存区域,每个进程都有自己的变量。为了进行通信,进程必须使用其他通道(文件、管道或套接字)。译注:

如果您想要并行化一个计算,那么您可能需要多线程,因为您可能希望线程在相同的内存上进行协作。

谈到性能,线程的创建和管理比进程更快(因为操作系统不需要分配一个全新的虚拟内存区域) ,线程间的通信通常比行程间通讯更快。但是线程更难编程。线程可以互相干扰,也可以写入对方的内存,但是这种情况的发生方式并不总是显而易见的(由于几个因素,主要是指令重排序和内存缓存) ,因此需要同步原语来控制对变量的访问。

大部分问题都是由朱利奥 · 弗兰科回答的。我将进一步阐述消费者-生产者问题,我认为这将为您使用多线程应用程序的解决方案指明正确的方向。

fill_count = Semaphore(0) # items produced
empty_count = Semaphore(BUFFER_SIZE) # remaining space
buffer = Buffer()


def producer(fill_count, empty_count, buffer):
while True:
item = produceItem()
empty_count.down();
buffer.push(item)
fill_count.up()


def consumer(fill_count, empty_count, buffer):
while True:
fill_count.down()
item = buffer.pop()
empty_count.up()
consume_item(item)

您可以从以下网站了解更多关于同步原语的信息:

 http://linux.die.net/man/7/sem_overview
http://docs.python.org/2/library/threading.html

伪代码在上面。我想您应该搜索生产者-消费者-问题以获得更多的引用。

Giulio Franco 说的适用于多线程与多处理 一般来说

然而,Python/*还有一个额外的问题: 有一个 GIL 阻止同一进程中的两个线程同时运行 Python 代码。这意味着如果你有8个内核,并改变你的代码使用8个线程,它将不能使用800% 的 CPU 和运行8倍的速度; 它将使用相同的100% 的 CPU 和运行在相同的速度。(实际上,它会运行得慢一些,因为即使没有共享数据,线程也会产生额外的开销,但是现在先忽略这一点。)

但也有例外。如果您的代码的大量计算实际上并不发生在 Python 中,而是发生在一些具有适当 GIL 处理的定制 C 代码的库中,比如一个简单的应用程序,那么您将从线程处理中获得预期的性能收益。如果繁重的计算是由您运行并等待的某个子进程完成的,也是如此。

更重要的是,在某些情况下,这并不重要。例如,网络服务器花费大部分时间从网络上读取数据包,GUI 应用程序花费大部分时间等待用户事件。在网络服务器或 GUI 应用程序中使用线程的一个原因是允许您执行长时间运行的“后台任务”,而无需停止主线程继续服务网络数据包或 GUI 事件。这在 Python 线程中可以很好地工作。(在技术术语中,这意味着 Python 线程提供并发性,即使它们不提供核心并行性。)

但是如果您使用纯 Python 编写 CPU 绑定程序,那么使用更多的线程通常是没有帮助的。

使用单独的进程对于 GIL 没有这样的问题,因为每个进程都有自己的单独的 GIL。当然,线程和进程之间的权衡还是和其他任何语言一样ーー在进程之间共享数据比在线程之间共享数据更加困难和昂贵,运行大量进程或者频繁地创建和销毁它们可能会很昂贵,等等。但是 GIL 在进程之间的平衡中占有重要的地位,而在某种程度上,对于 C 或 Java 来说,情况并非如此。因此,您会发现自己在 Python 中使用多重处理的次数要比在 C 或 Java 中多得多。


与此同时,Python 的“包含电池”理念带来了一些好消息: 编写代码非常容易,只需更改一行代码就可以在线程和进程之间来回切换。

如果你的代码是按照自包含的“作业”设计的,除了输入和输出之外,它们与其他作业(或主程序)不共享任何东西,你可以使用 concurrent.futures库围绕一个线程池编写代码,如下所示:

with concurrent.futures.ThreadPoolExecutor(max_workers=4) as executor:
executor.submit(job, argument)
executor.map(some_function, collection_of_independent_things)
# ...

您甚至可以获得这些作业的结果,并将它们传递给其他作业,按执行顺序或完成顺序等待,有关详细信息,请参阅 Future对象部分。

现在,如果发现您的程序一直在使用100% 的 CPU,并且添加更多的线程只会使它变慢,那么您就遇到了 GIL 问题,因此您需要切换到进程。你只需要改一下第一行:

with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:

唯一真正需要注意的是,您的作业的参数和返回值必须是可 pickle 的(而且不需要花费太多的时间或内存来 pickle) ,才能成为可用的跨进程。通常这不是问题,但有时是。


但如果你的工作不能自给自足呢?如果可以根据 传递信息从一个作业到另一个作业来设计代码,那么仍然非常容易。您可能必须使用 threading.Threadmultiprocessing.Process,而不是依赖于池。您必须显式地创建 queue.Queuemultiprocessing.Queue对象。(还有很多其他选项ーー管道、套接字、带有群的文件,... ... 但关键是,如果“执行者”的自动魔力不足,那么必须手动执行 什么东西。)

但是,如果您甚至不能依靠消息传递呢?如果您需要两个作业来变更同一个结构,并查看彼此的更改,该怎么办?在这种情况下,需要进行手动同步(锁、信号量、条件等) ,如果希望使用进程,还需要引导显式的共享内存对象。这就是多线程(或多处理)变得困难的时候。如果你能避免,那很好; 如果你做不到,那么你需要阅读的内容将超过某人能够给出的 SO 答案。


通过注释,您想知道 Python 中线程和进程之间的区别。真的,如果你读了 Giulio Franco 的答案和我的,还有我们所有的链接,这应该涵盖了所有的东西... ... 但是一个总结肯定是有用的,所以这里是:

  1. 默认情况下,线程共享数据; 进程不共享数据。
  2. 由于(1) ,在进程之间发送数据通常需要 pickle 和 unpickle
  3. 作为(1)的另一个结果,在进程之间直接共享数据通常需要将其放入低级格式,如 Value、 Array 和 ctypes类型。
  4. 进程不受 GIL 的约束。
  5. 在某些平台(主要是 Windows)上,创建和销毁进程的成本要高得多。
  6. 对于进程有一些额外的限制,其中一些限制在不同的平台上是不同的。
  7. threading模块没有 multiprocessing模块的一些特性。(您可以使用 multiprocessing.dummy在线程之上获取大部分缺失的 API,或者您可以使用更高级别的模块(如 concurrent.futures)而不必担心它。)

* 有这个问题的实际上不是 Python 语言,而是 CPython 语言的“标准”实现。其他一些实现没有 GIL,比如 Jython。

* * 如果您使用的是用于多处理的开始方法(在大多数非 Windows 平台上都可以使用) ,那么每个子进程都可以获得启动子进程时父进程拥有的任何资源,这可以是另一种将数据传递给子进程的方法。

我相信 这个链接以一种优雅的方式回答了你的问题。

简而言之,如果一个子问题必须等待另一个完成,多线程是好的(例如,在 I/O 重型操作中) ; 相比之下,如果您的子问题确实可以同时发生,则建议使用多线程。但是,您不会创建比核心数量更多的进程。

下面是 python 2.6. x 的一些性能数据,它们调用来质疑在 IO 绑定场景中线程比多处理性能更好的概念。这些结果来自40处理器的 IBMSystemx3650M4BD。

IO 绑定处理: 进程池的性能优于线程池

>>> do_work(50, 300, 'thread','fileio')
do_work function took 455.752 ms


>>> do_work(50, 300, 'process','fileio')
do_work function took 319.279 ms

CPU 绑定处理: 进程池的性能优于线程池

>>> do_work(50, 2000, 'thread','square')
do_work function took 338.309 ms


>>> do_work(50, 2000, 'process','square')
do_work function took 287.488 ms

这些都不是严格的测试,但它们告诉我,与线程相比,多处理并不是完全没有性能。

交互式 Python 控制台中用于上述测试的代码

from multiprocessing import Pool
from multiprocessing.pool import ThreadPool
import time
import sys
import os
from glob import glob


text_for_test = str(range(1,100000))


def fileio(i):
try :
os.remove(glob('./test/test-*'))
except :
pass
f=open('./test/test-'+str(i),'a')
f.write(text_for_test)
f.close()
f=open('./test/test-'+str(i),'r')
text = f.read()
f.close()




def square(i):
return i*i


def timing(f):
def wrap(*args):
time1 = time.time()
ret = f(*args)
time2 = time.time()
print '%s function took %0.3f ms' % (f.func_name, (time2-time1)*1000.0)
return ret
return wrap


result = None


@timing
def do_work(process_count, items, process_type, method) :
pool = None
if process_type == 'process' :
pool = Pool(processes=process_count)
else :
pool = ThreadPool(processes=process_count)
if method == 'square' :
multiple_results = [pool.apply_async(square,(a,)) for a in range(1,items)]
result = [res.get()  for res in multiple_results]
else :
multiple_results = [pool.apply_async(fileio,(a,)) for a in range(1,items)]
result = [res.get()  for res in multiple_results]




do_work(50, 300, 'thread','fileio')
do_work(50, 300, 'process','fileio')


do_work(50, 2000, 'thread','square')
do_work(50, 2000, 'process','square')

Python 文档引用

我在 CPython 的 GIL 是什么?中强调了关于 Process vs Threads 和 GIL 的关键 Python 文档引用

进程与线程实验

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

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

结果是:

enter image description here

绘图数据.

结论:

  • 对于 CPU 绑定的工作,多处理总是更快,可能是由于 GIL

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

  • 由于我使用的是一台8超线程机器,因此线程只能扩展到大约4倍,而不是预期的8倍。

    相比之下,C POSIX CPU 绑定工作达到了预期的8倍加速: 在时间输出(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 上游 + 在同一目录上绘制代码.

在 Ubuntu 18.10,Python 3.6.7,联想 ThinkPad p51笔记本电脑上进行测试,CPU: 英特尔酷睿 i7-7820hQ CPU (4核/8线程) ,RAM: 2 x Samsung m471a2k43BB1-CRC (2 x 16giB) ,固态硬盘: Samsung MZVLB512HAJQ-000L7(3,000 MB/s)。

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

这篇文章 https://rohanvarma.me/GIL/告诉我,你可以运行一个回调,无论什么时候一个线程计划与 ABC1的 target=参数multiprocessing.Process相同。

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

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

这表明:

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