- 原文地址:Intro to Threads and Processes in Python
- 原文做者:Brendan Fortuner
- 译文出自:掘金翻译计划
- 本文永久连接:github.com/xitu/gold-m…
- 译者:lsvih
- 校对者:yqian1991
在参加 Kaggle 的 Understanding the Amazon from Space 比赛时,我试图对本身代码的各个部分进行加速。速度在 Kaggle 比赛中相当重要。高排名经常须要尝试数百种模型结构与超参组合,能在一个持续一分钟的 epoch 中省出 10 秒都是一个巨大的胜利。html
让我吃惊的是,数据处理是最大的瓶颈。我用了 Numpy 的矩阵旋转、矩阵翻转、缩放及裁切等操做,在 CPU 上进行运算。Numpy 和 Pytorch 的 DataLoader 在某些状况中使用了并行处理。我同时会运行 3 到 5 个实验,每一个实验都各自进行数据处理。但这种处理方式看起来效率不高,我但愿知道我是否能用并行处理来加快全部实验的运行速度。前端
简单来讲就是在同一时刻作两件事情,也能够是在不一样的 CPU 上分别运行代码,或者说当程序等待外部资源(文件加载、API 调用等)时把“浪费”的 CPU 周期充分利用起来提升效率。python
下面的例子是一个“正常”的程序。它会使用单线程,依次进行下载一个 URL 列表的内容。linux
下面是一个一样的程序,不过使用了 2 个线程。它把 URL 列表分给不一样的线程,处理速度几乎翻倍。android
若是你对如何绘制以上图表感到好奇,能够参考源码,下面也简单介绍一下:ios
URLS = [url1, url2, url3, ...]
def download(url, base):
start = time.time() - base
resp = urlopen(url)
stop = time.time() - base
return start,stop
复制代码
results = [download(url, 1) for url in URLS]
复制代码
def visualize_runtimes(results):
start,stop = np.array(results).T
plt.barh(range(len(start)), stop-start, left=start)
plt.grid(axis=’x’)
plt.ylabel("Tasks")
plt.xlabel("Seconds")
复制代码
多线程的图表生成方式与此相似。Python 的并发库同样能够返回结果数组。git
一个进程就是一个程序的实例(好比 Jupyter notebook 或 Python 解释器)。进程启动线程(子进程)来处理一些子任务(好比按键、加载 HTML 页面、保存文件等)。线程存活于进程内部,线程间共享相同的内存空间。github
举例:Microsoft Word
当你打开 Word 时,你其实就是建立了一个进程。当你开始打字时,进程启动了一些线程:一个线程专门用于获取键盘输入,一个线程用于显示文本,一个线程用于自动保存文件,还有一个线程用于拼写检查。在启动这些线程以后,Word 就能更好的利用空闲的 CPU 时间(等待键盘输入或文件加载的时间)让你有更高的工做效率。编程
*
)CPU,或者说处理器,管理着计算机最基本的运算工做。CPU 有一个或着多个核,可让 CPU 同时执行代码。后端
若是只有一个核,那么对 CPU 密集型任务(好比循环、运算等)不会有速度的提高。操做系统须要在很小的时间片在不一样的任务间来回切换调度。所以,作一些很琐碎的操做(好比下载一些图片)时,多任务处理反而会下降处理性能。这个现象的缘由是在启动与维护多个任务时也有性能的开销。
CPython(python 的标准实现)有一个叫作 GIL(全局解释锁)的东西,会阻止在程序中同时执行两个线程。一些人很是不喜欢它,但也有一些人喜欢它。目前有一些解决它的方法,不过 Numpy 之类的库大都是经过执行外部 C 语言代码来绕过这种限制。
*
对于点积等某些运算,Numpy 绕过了 Python 的 GIL 锁,可以并行执行代码。
Python 的 concurrent.futures 库用起来轻松愉快。你只须要简单的将函数、待处理的对象列表和并发的数量传给它便可。在下面几节中,我会以几种实验来演示什么时候使用线程什么时候使用进程。
def multithreading(func, args,
workers):
with ThreadPoolExecutor(workers) as ex:
res = ex.map(func, args)
return list(res)
def multiprocessing(func, args,
workers):
with ProcessPoolExecutor(work) as ex:
res = ex.map(func, args)
return list(res)
复制代码
对于 API 调用,多线程明显比串行处理与多进程速度要快不少。
def download(url):
try:
resp = urlopen(url)
except Exception as e:
print ('ERROR: %s' % e)
复制代码
2 个线程
4 个线程
2 个进程
4 个进程
我传入了一个巨大的文本,以观测线程与进程的写入性能。线程效果较好,但多进程也让速度有所提高。
def io_heavy(text):
f = open('output.txt', 'wt', encoding='utf-8')
f.write(text)
f.close()
复制代码
串行
%timeit -n 1 [io_heavy(TEXT,1) for i in range(N)]
>> 1 loop, best of 3: 1.37 s per loop
复制代码
4 个线程
4 个进程
因为没有 GIL,能够在多核上同时执行代码,多进程理所固然的胜出。
def cpu_heavy(n):
count = 0
for i in range(n):
count += i
复制代码
串行: 4.2 秒
4 个线程: 6.5 秒
4 个进程: 1.9 秒
不出所料,不管是用多线程仍是多进程都不会对此代码有什么帮助。Numpy 在幕后执行外部的 C 语言代码,绕开了 GIL。
def dot_product(i, base):
start = time.time() - base
res = np.dot(a,b)
stop = time.time() - base
return start,stop
复制代码
串行: 2.8 秒
2 个线程: 3.4 秒
2 个进程: 3.3 秒
以上实验的 Notebook 请参考此处,你能够本身来复现这些实验。
如下是我在探索这个主题时的一些参考文章。特别推荐 Nathan Grigg 的这篇博客,给了我可视化方法的灵感。
若是发现译文存在错误或其余须要改进的地方,欢迎到 掘金翻译计划 对译文进行修改并 PR,也可得到相应奖励积分。文章开头的 本文永久连接 即为本文在 GitHub 上的 MarkDown 连接。
掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 Android、iOS、前端、后端、区块链、产品、设计、人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划、官方微博、知乎专栏。