在本教程中,我们将学习如何使用Python实现多线程和多处理方法。这些方法指导操作系统优化使用系统硬件,从而提高代码执行效率。
多线程引用Wiki的解释—在计算机体系结构中,多线程是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,进而提升整体处理性能。
并发指的是可以实现多个进程的并行执行,从而实现更快的运行时间。当执行基于I/O的任务(如下载图像和文件)时,多线程是更有效的,另一方面多处理也适合于基于CPU的计算密集型任务。
Python中的多线程实现
为了实现多线程,我们将使用Python的标准库threading。默认情况下,该库Python会默认安装,因此可以直接在代码中导入。
为了演示多线程的有效性,我们将从Unsplash下载5幅图像。让我们观察一下当我们按顺序下载这些图像时的执行时间:#### 导入请求库import requests
#### 定义函数def down_img(name,link): data = requests.get(link).content name = f"/home/isud/DidYouKnow/Tutorial 5/{name}.jpg" with open(name, "wb") as file: file.write(data)
#### 连续下载5张图片%%timeit -n1 -r1images = ['https://images.unsplash.com/photo-1531458999205-f31f14fa217b', 'https://images.unsplash.com/photo-1488572749058-7f52dd70e0fa', 'https://images.unsplash.com/photo-1531404610614-68f9e73e35db', 'https://images.unsplash.com/photo-1523489405193-3884f5ca475f', 'https://images.unsplash.com/photo-1565098735462-5db3412ac4cb']for i,link in enumerate(images): down_img(i,link)
#### %%timeit results51.4 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
可以观察到,5张图片的完整下载耗时51.4秒,而且只有在上一次下载结束后才开始新的下载。现在让我们看看多线程如何提高代码性能。#### 导入必要的库import threadingimport requests
#### 定义函数def down_img(name,link): data = requests.get(link).content name = f"/home/isud/DidYouKnow/Tutorial 5/{name}.jpg" with open(name, "wb") as file: file.write(data)
#### 并行线程下载图像%%timeit -n1 -r1threads = []images = ['https://images.unsplash.com/photo-1531458999205-f31f14fa217b', 'https://images.unsplash.com/photo-1488572749058-7f52dd70e0fa', 'https://images.unsplash.com/photo-1531404610614-68f9e73e35db', 'https://images.unsplash.com/photo-1523489405193-3884f5ca475f', 'https://images.unsplash.com/photo-1565098735462-5db3412ac4cb']for i,link in enumerate(images): t = threading.Thread(target=down_img, args=(i,link)) t.start() threads.append(t)
for thread in threads: thread.join()
#### %%timeit results25.6 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
代码解释-定义图像下载循环:第1步(线程初始化)——Python在一个线程中运行完整的代码(我们称之为主线程)。在本例中,通过从线程库调用Thread函数,我们启动并行线程并为它们分配一个要执行的目标进程(在本例中为down_image)。被调用函数所需的所有参数都应作为序列对象(在本例中为元组)传递。对Thread函数的每次调用都会启动一个新线程(我们称之为并行线程)。
第2步(线程启动)——调用线程的start方法将指示Python启动线程执行。如果for循环在主线程中执行,而函数调用在并行线程中,则for循环的执行将在图片下载过程中继续执行。
步骤3(线程的join)——每个新线程都被捕获到一个名为threads的列表中,然后通过调用join方法将并行线程连接到主线程。
为什么需要使用join?
在第2步之前,我们所有的线程(主线程和并行线程)都是并行执行的,在这种情况下,主线程完成任务的时间可以比并行线程完成任务早很多,及主线程会结束更早。
为了避免这种情况,将并行线程连接到主线程是必须的,这将确保只有在并行线程完成之后才完成主线程的执行。下图说明了这两种情况:
可以看出,下载图像的执行时间减少了近50%(大约25.6秒)。上面的示例展示了多线程在I/O操作中的帮助,以及如何提高下载/上传过程的效率。
多处理
与在单个进程中执行多个线程的多线程不同,多处理为每个任务启动一个新的并行进程。如前所述,它为CPU密集型任务(需要大量计算的任务)提供了相当大的运行时改进。
在Python中实现多处理
multiprocessing是另一个在Python中支持多处理特性的标准库,为了理解它的功能,我们将多次调用一个计算密集型函数,来计算从1到1千万的数字的平方。此函数并行执行8次,让我们观察一下这个函数在正常情况下的性能。#### 导入时间库import time
#### 定义函数def demo_func(num): for i in range(num): a = i**2
#### 顺序调用演示函数%%timeit -n1 -r1for i in range(8): demo_func(10000000)
#### %%timeit 结果21.2 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
演示函数的顺序执行总共花费了21.2秒,现在让我们检查在多处理设置中执行此操作时的性能提升。#### 导入库import time
#### 定义函数def demo_func(num): for i in range(num): a = i**2
#### 多处理demo函数%%timeit -n1 -r1processes = []lop_size = [10000000,10000000,10000000,10000000,10000000,10000000,10000000, 10000000]p = multiprocessing.Pool()p.map(demo_func,lop_size)p.close()p.join()
#### %%timeit 结果11.6 s ± 0 ns per loop (mean ± std. dev. of 1 run, 1 loop each)
在多处理框架下,执行时间减少了50%,达到11.6秒。在顺序处理中,一次使用一个CPU内核,而在多个处理中,所有系统内核都是并行使用的。CPU使用率屏幕截图显示了相同的情况:
上图中的每一行代表一个CPU核。请注意,在顺序执行中,每个函数调用都会触发一个核,而在并行执行中,所有核都是同时触发的。
代码说明步骤1(池创建)-池方法创建可并行利用的进程池。在没有任何参数的情况下,创建的进程数等于系统上的CPU核数。我有一个四核系统,这意味着,在执行时的8个函数调用中,前4个调用将并行运行,然后是下4个函数调用。请注意,你还可以在池中定义一个自定义的进程数(多于内核数),但超过某个值时,它将开始占用系统内存并可能降低性能
步骤2(池映射)-这是指示进程执行特定函数(第一个参数)以及要传递给它的参数列表(第二个参数)
步骤3(Pool Close)-Close方法指示Python解释器,我们已经提交了要提交给池的所有内容,将来不再向池提供更多的输入。
步骤4(池连接)-与线程的情况一样,Join方法确保代码执行只在所有并行进程完成后完成。
从上面的场景中,我们可以看到多处理是如何在高效的代码性能方面成为一个很好的帮手。
结束
本教程中,我们将重点放在通过优化系统硬件来提高代码性能上。希望这篇教程能帮助你,你能学到一些新东西。
参考链接:https://towardsdatascience.com/did-you-know-how-to-make-your-python-code-run-faster-1st-installment-f317359159a1