加速的方法
对于加速程序速度,有两个思路,对于一个任务量固定的程序而言
- 同一时刻计算的数据量更多
- 单次运算计算的数据量更多
前者可以通过使用线程或者进程来进行实现,后者则大部分需要通过指令集来进行实现。这篇文章也主要讲解前者如何加速你的程序。
为什么这样可以加速
这里简单讲一下为什么上述的两种方法可以实现程序加速。对于进程或线程而言,由于CPU拥有多个核心,通过进程或者线程可以将任务分散到CPU的不同核心上,相对于不使用线程或进程的程序而言,相当于将原来只有一个人干的工作现在分给了好几个人去完成;对于指令集可以实现加速是因为,在CPU进行运算的时候,是以二进制进行运算,由于有些数据类型(比如一个长整型有long64位比特位)在实际参与运算的过程中有些比特位并没有数据,但是这一部分也会参与运算,这就造成了资源的浪费,指令集可以做到将几个小的数据(短比特位)汇总成一个长比特位的数据类型,从而实现计算一个长整型的数据就可以同时计算好几个短整型数据,指令集除了支持整型数据之外,也可以支持浮点类型的数据。
线程和进程
线程和进程是两个十分相似的概念,在不同的操作系统中也有区别,比如在Windows系统中有真正的线程和进程,而在Linux系统中只有进程而没有真正的线程(是由进程模拟出来的)。对于操作系统而言,进程是最小的调度单位,调度也即将一个计算任务放到CPU的那个核上面去执行,由于是在不同的核上面运行,也就导致了不同的的进程之间运行是互不影响的,不同进程之间的资源也无法做到共享。线程由于不同的编程语言实现的方式不同,也有差别,对于C语言而言,线程也可以通过调度将多个线程分配到多个CPU的核上,而对于Python语言,由于Python在实现过程中(Cpython)人为的引入了GIL(全局解释器锁),使得Python的多线程程序在运行的时候,同一时刻只能运行一个线程,且同一时刻只能占用CPU的一个核,造成了一核有难、八核围观的窘境。这样一来对于Python而言多线程程序貌似是没有加速程序的作用,但是请注意,这里只是CPU会“卡顿”,对于一些程序不只是只有计算任务,还有读取和写入(IO操作)的任务,如果程序的限速步骤是读取和写入数据,那么Python使用多线程依旧可以做到加速程序的效果。所以对于Python而言,如果是一个IO密集型的程序,完全可以使用多线程来进行加速,如果是计算密集型的程序,使用多线程可能不会对你的程序性能有太大的提高,但是你可以使用Python的多进程来完成计算密集型的任务,Python多进程可以将任务分配到CPU不同的核上,不会有锁的限制。
Python多进程与多线程
在python中有几个与多进程和多线程相关的库
threading
multiprocessing
queue
subprocess
concurrent.futures
如果你想快速上手多进程和多线程,那么我会推荐你首先学习concurrent.futures
,这是一个Python官方封装好的非常容易上手的进程/线程池,使用它可以很方便的将一个常规的任务改造成多线程/多进程版本。
核心是一个「ProcessPoolExecutor」对象(多线程版本的是「ThreadPoolExecutor」),首先进行实例化得到一个**executor,**这里有两种方法,一种直接进行实例化,一种是使用with
进行上下文管理。
# 直接进行实例化 # 创建8个进程 executor = ProcessPoolExecutor(8) executor.shutdown() # 关闭进程(强行关闭进程) # executor.shutdown(wait=True) # 等待所有的进程都执行完毕,后再退出 # 使用with with ProcessPoolExecutor(8) as executor: # 这里就不用主动调用shutdown方法了,with可以自动关闭 pass
这样就创建好了进程池
往进程池中有两种方式投递任务
- 「map 一次性投递多个任务」
- 「submit 一次投递一个任务」
虽然有两种不同的方式,其实核心的方法是submit
,map
方法内部是将多个任务逐个的使用submit
来提交任务。具体的参数也几乎一样,都需要传入一个要执行的函数和函数对应的参数。
# submit work = executor.submit(work_fn, arg) # 需要调用work的result方法来来获取结果 work_result = work.result() # map works_reult = executor.map(work_fn, args) # map直接可以返回结果
下面将一段常规任务,改造成他的多进程版本
- 常规版本
files_path = [ '1.txt', '2.txt', '3.txt' ] def make_zipfile(file_path, save_path): """ 给定一个文件路径,将其压缩成压缩文件 然后保存到一个具体的目录 """ # 具体的压缩逻辑 pass # 使用循环逐个的进行压缩 for file_path in files_path: make_zipfile(file_path)
- 多进程版本
import os from concurrent.futures import ProcessPoolExecutor files_path = [ '1.txt', '2.txt', '3.txt' ] def make_zipfile(arg): """ 给定一个文件路径,将其压缩成压缩文件 """ # 这里的函数只有一个参数,是为了往进程池投递任务方便传参数 [file_path, save_path] = arg # 具体的压缩逻辑 pass # 使用map args = [(file_path, file_path + '.zip') for i in files_path] process_count = os.cpu_count() with ProcessPoolExecutor(process_count) as executor: result = executor.map(make_zipfile, args) # 使用submit args = [(file_path, file_path + '.zip') for i in files_path] process_count = os.cpu_count() with ProcessPoolExecutor(process_count) as executor: reuslt = [] for arg in args: result.append(executor.submit(make_zipfile, arg)) [i.result() for i in result] # 主动去调用result方法去获取函数的返回值
相对应的多线程只需要将「ProcessPoolExecutor」更换成「ThreadPoolExecutor」即可。唯一需要注意的是,要根据任务的类型是以一些数学计算为主,还是以IO(读取文件,写入文件)为主,来去选择是使用多线程还是多进程。
当你后面的任务越来越复杂的时候,可能上面这种方法就不再适合你的任务需求,那么你就需要去学习「threading」和**multiprocessing **具体该如何使用。
最后给自己挖个坑,规划加速系列出四篇推文,本文是第一篇
「加速你的Python程序(线程/进程池)」
加速你的Python程序(线程/进程)
加速你的Python程序(内存)
加速你的Python程序(Python调用C)