我正在尝试理解multiprocessing相比threading的优势。我知道multiprocessing可以绕过全局解释器锁,但除此之外还有哪些优势?threading不能做同样的事情吗?
我正在尝试理解multiprocessing相比threading的优势。我知道multiprocessing可以绕过全局解释器锁,但除此之外还有哪些优势?threading不能做同样的事情吗?
以下是我总结的一些优缺点。
multiprocessing
模块包含有用的抽象,接口类似于 threading.Thread
Queue
模块),则需要手动使用同步原语(需要决定锁定的粒度)threading
模块使用线程,multiprocessing
模块使用进程。区别在于:线程在同一内存空间中运行,而进程具有单独的内存空间。这使得使用 multiprocessing 在进程之间共享对象变得有些困难。由于线程使用相同的内存,必须采取预防措施,否则两个线程将同时写入同一内存。这就是全局解释器锁的作用。
生成进程比生成线程略慢一些。
多线程的作用是使应用程序具有响应能力。假设您有一个数据库连接并且需要响应用户输入,如果没有使用多线程,如果数据库连接繁忙,则应用程序将无法响应用户。通过将数据库连接拆分为单独的线程,可以使应用程序更具响应性。此外,由于这两个线程在同一进程中,它们可以访问相同的数据结构 - 良好的性能,加上灵活的软件设计。
请注意,由于GIL的原因,应用程序实际上并没有同时执行两个任务,但我们已经将数据库资源锁定放入了一个单独的线程中,以便可以在用户交互和CPU时间之间进行切换。CPU时间在线程之间进行分配。
多进程适用于真正需要同时处理多个任务的情况。假设您的应用程序需要连接到6个数据库并对每个数据集执行复杂的矩阵转换。将每个作业放入单独的线程中可能会有所帮助,因为当一个连接处于空闲状态时,另一个连接可以获得一些CPU时间,但处理不会并行进行,因为GIL意味着您只使用一个CPU的资源。通过将每个作业放入多进程进程中,每个作业都可以在自己的CPU上运行并以全效率运行。
Python文档引用
此答案的规范版本现在位于重复的问题中:什么是线程和多进程模块之间的差异?
我已经突出了关于进程与线程以及GIL的关键Python文档引用:CPython中的全局解释器锁(GIL)是什么?
进程与线程实验
我进行了一些基准测试,以便更具体地展示差异。
在基准测试中,我计时了在8个超线程 CPU上使用各种线程数量进行CPU和IO绑定工作。每个线程提供的工作总是相同的,因此更多的线程意味着更多的总工作量。
结果如下:
数据绘图。
结论:
对于CPU绑定的工作,多进程始终更快,可能是由于GIL。
对于IO绑定的工作,两者速度完全相同。
线程仅扩展到大约4倍,而不是期望的8倍,因为我使用的是8超线程机器。
与C POSIX CPU绑定的工作形成对比,后者达到了预期的8倍加速:在time(1)输出中,“real”,“user”和“sys”是什么意思?
待办事项:我不知道原因,必须有其他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。
在Ubuntu 18.10,Python 3.6.7上测试通过,在Lenovo ThinkPad P51笔记本电脑上使用CPU:Intel Core i7-7820HQ CPU(4个核心/8个线程),RAM:2x Samsung M471A2K43BB1-CRC(2x 16GiB),SSD:Samsung MZVLB512HAJQ-000L7(3,000 MB/s)。
可视化给定时间正在运行的线程
这篇文章https://rohanvarma.me/GIL/教会了我,你可以使用threading.Thread
的target=
参数以及multiprocessing.Process
的相同参数在调度线程时运行回调函数。
这使我们能够准确地查看每个时间运行的线程。当这样做时,我们会看到类似于以下内容(我制作了这个特定的图表):
+--------------------------------------+
+ Active threads / processes +
+-----------+--------------------------------------+
|Thread 1 |******** ************ |
| 2 | ***** *************|
+-----------+--------------------------------------+
|Process 1 |*** ************** ****** **** |
| 2 |** **** ****** ** ********* **********|
+-----------+--------------------------------------+
+ Time --> +
+--------------------------------------+
这将表明:
关键优势在于隔离性。崩溃的进程不会使其他进程崩溃,而崩溃的线程可能会对其他线程造成严重影响。
如问题所述,Python中的多进程(Multiprocessing)是实现真正并行的唯一方法。而多线程(Multithreading)无法实现这一点,因为GIL会阻止线程并行运行。
因此,在Python中,线程可能并不总是有用的,事实上,根据您尝试实现的内容,它甚至可能导致性能变差。例如,如果您正在执行计算密集型任务,如解压缩gzip文件或3D渲染(任何CPU密集型任务),则线程可能会妨碍您的性能而非帮助。在这种情况下,你需要使用多进程(Multiprocessing),因为只有该方法实际上可以并行运行,并有助于分配任务负载。但这可能会有一些开销,因为多进程(Multiprocessing)涉及将脚本内存复制到每个子进程中,这可能会对大型应用程序造成问题。
然而,当您的任务是IO-bound时,多线程(Multithreading)变得有用。例如,如果您的大部分任务涉及等待 API调用,则应使用多线程(Multithreading),因为在等待时启动另一个线程中的请求,而不是让您的CPU闲置。
简而言之:
还有一件事没有提到,那就是速度取决于您使用的操作系统。在Windows中,进程是昂贵的,因此在Windows中使用线程会更好,但在Unix中,进程比它们的Windows变体更快,因此在Unix中使用进程更安全且能够快速生成。
multiprocessing.Process
。多进程
- Python中的multiprocessing库使用单独的内存空间、多个CPU核心、绕过CPython中的GIL限制,子进程可以被“杀死”(例如程序中的函数调用),使用起来更加方便。
- 该模块的一些注意事项包括较大的内存占用和IPC的一些更复杂的开销。
多线程
- 多线程库是轻量级的,共享内存,负责响应式UI,适用于I/O绑定应用程序。
- 该模块无法“杀死”线程,受GIL的影响。
- 多个线程存在于同一进程的同一空间中,每个线程将执行特定的任务,具有自己的代码、栈内存、指令指针和共享堆内存。
- 如果一个线程有内存泄漏,它可能会损坏其他线程和父进程。
使用Python进行多线程和多进程的示例
Python 3具有启动并行任务的功能。这使我们的工作更加容易。
以下是一个示例:
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)))
进程池执行器
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()
ThreadPoolExecutor
和ProcessPoolExecutor
,效果会更好。 - undefinedProcessPoolExecutor
,然后将 with concurrent.futures.ProcessPoolExecutor() as executor:
改为 with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
。 - undefined线程共享同一内存空间,以确保两个线程不共享相同的内存位置,因此需要采取特殊预防措施。CPython解释器使用一种称为全局解释器锁(GIL)的机制来处理这个问题。
GIL是什么?
在CPython中,全局解释器锁(GIL)是一个互斥锁,用于保护Python对象的访问,防止多个线程同时执行Python字节码。这个锁主要是必需的,因为CPython的内存管理不是线程安全的。
针对主要问题,我们可以通过使用用例进行比较,如何比较?
1-线程使用情况:在GUI程序中,线程可用于使应用程序具有响应性。例如,在文本编辑程序中,一个线程可以负责记录用户输入,另一个线程可以负责显示文本,第三个线程可以进行拼写检查等等。在这种情况下,程序必须等待用户交互,这是最大的瓶颈。线程的另一个使用情况是那些IO绑定或网络绑定的程序,例如网络爬虫。
2-多处理使用情况:在程序需要CPU密集型处理而不需要进行任何IO或用户交互的情况下,多处理优于线程。