python并行计算
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
python并⾏计算
⼀、进程和线程
原⽂链接:
进程是分配资源的最⼩单位,线程是系统调度的最⼩单位。
当应⽤程序运⾏时最少会开启⼀个进程,此时计算机会为这个进程开辟独⽴的内存空间,不同的进程享有不同的空间,⽽⼀个CPU在同⼀时刻只能够运⾏⼀个进程,其他进程处于等待状态。
⼀个进程内部包括⼀个或者多个线程,这些线程共享此进程的内存空间与资源。
相当于把⼀个任务⼜细分成若⼲个⼦任务,每个线程对应⼀个⼦任务。
⼆、多进程和多线程
对于⼀个CPU来说,在同⼀时刻只能运⾏⼀个进程或者⼀个线程,⽽单核CPU往往是在进程或者线程间切换执⾏,每个进程或者线程得到⼀定的CPU时间,由于切换的速度很快,在我们看来是多个任务在并⾏执⾏(同⼀时刻多个任务在执⾏),但实际上是在并发执⾏(⼀段时间内多个任务在执⾏)。
单核CPU的并发往往涉及到进程或者线程的切换,进程的切换⽐线程的切换消耗更多的时间与资源。
在单核CPU下,CPU密集的任务采⽤多进程或多线程不会提升性能,⽽在IO密集的任务中可以提升(IO阻塞时CPU空闲)。
可以给CPU密集型任务和IO密集型任务配置⼀些线程数。
CPU密集型:线程个数为CPU核数。
这⼏个线程可以并⾏执⾏,不存在线程切换到开销,提⾼了cpu的利⽤率的同时也减少了切换线程导致的性能损耗。
IO密集型:线程个数为CPU核数的两倍。
到其中的线程在IO操作的时候,其他线程可以继续⽤cpu,提⾼了cpu的利⽤率。
⽽多核CPU就可以做到同时执⾏多个进程或者多个进程,也就是并⾏运算。
在拥有多个CPU的情况下,往往使⽤多进程或者多线程的模式执⾏多个任务。
三、python中的多进程和多线程
1、多进程
def Test(pid):
print("当前进程{}:{}".format(pid, os.getpid()))
for i in range(1000000000):
pass
if__name__ == '__main__':
#单进程
start = time.time()
for i in range(2):
Test(i)
end = time.time()
print((end - start))
添加多线程之后
def Test(pid):
print("当前⼦进程{}:{}".format(pid, os.getpid()))
for i in range(100000000):
pass
if__name__ == '__main__':
#多进程
print("⽗进程:{}".format(os.getpid()))
start = time.time()
pool = Pool(processes=2)
pid = [i for i in range(2)]
pool.map(Test, pid)
pool.close()
pool.join()
end = time.time()
print((end - start))
从输出结果可以看出都是执⾏两次for循环,多进程⽐单进程减少了近乎⼀半的时间(这⾥使⽤了两个进程),并且查看CPU情况可以看出多进程利⽤了多个CPU。
python中的多进程可以利⽤mulitiprocess模块的Pool类创建,利⽤Pool的map⽅法来运⾏⼦进程。
⼀般多进程的执⾏如下代码:
def Test(pid):
print("当前⼦进程{}:{}".format(pid, os.getpid()))
for i in range(100000000):
pass
if__name__ == '__main__':
#多进程
print("⽗进程:{}".format(os.getpid()))
pool = Pool(processes=2)
pid = [i for i in range(4)]
pool.map(Test, pid)
pool.close()
pool.join()
1、利⽤Pool类创建⼀个进程池,processes声明在进程池中最多可以运⾏⼏个⼦进程,不声明的情况下会⾃动根据CPU数量来设定,原则上进程池容量不超过CPU数量。
(出于资源的考虑,不要创建过多的进程)
2、声明⼀个可迭代的变量,该变量的长度决定要执⾏多少次⼦进程。
3、利⽤map()⽅法执⾏多进程,map⽅法两个参数,第⼀个参数是多进程执⾏的⽅法名,第⼆个参数是第⼆步声明的可迭代变量,⾥⾯的每⼀个元素是⽅法所需的参数。
这⾥需要注意⼏个点:
进程池满的时候请求会等待,以上述代码为例,声明了⼀个容量为2的进程池,但是可迭代变量有4个,那么在执⾏的时候会先创建两个⼦进程,此时进程池已满,等待有⼦进程执⾏完成,才继续处理请求;
⼦进程处理完⼀个请求后,会利⽤已经创建好的⼦进程继续处理新的请求⽽不会重新创建进程。
map会将每个⼦进程的返回值汇总成⼀个列表返回。
在所有请求处理结束后使⽤close()⽅法关闭进程池不再接受请求。
使⽤join()⽅法让主进程阻塞,等待⼦进程退出,join()⽅法要放在close()⽅法之后,防⽌主进程在⼦进程结束之前退出。
2、多线程
python的多线程模块⽤threading类进⾏创建
import time
import threading
import os
count = 0
def change(n):
global count
count = count + n
count = count - n
def run(n):
print("当前⼦线程:{}".format(threading.current_thread().name))
for i in range(10000000):
change(n)
if__name__ == '__main__':
print("主线程:{}".format(threading.current_thread().name))
thread_1 = threading.Thread(target=run, args=(3,))
thread_2 = threading.Thread(target=run, args=(10,))
thread_1.start()
thread_2.start()
thread_1.join()
thread_2.join()
print(count)
程序执⾏会创建⼀个进程,进程会默认启动⼀个主线程,使⽤threading.Thread()创建⼦线程;target为要执⾏的函数;args传⼊函数需要的参数;start()启动⼦线程,join()阻塞主线程先运⾏⼦线程。
由于变量由多个线程共享,任何⼀个线程都可以对于变量进⾏修改,如果同时多个线程修改变量就会出现错误。
上⾯的程序在理论上的结果应该为0,但运⾏结果如图4。
出现这个结果的原因就是多个线程同时对于变量修改,在赋值时出现错误,具体解释见解决这个问题就是在修改变量的时候加锁,这样就可以避免出现多个线程同时修改变量。
import time
import threading
import os
count = 0
lock = threading.Lock()
def change(n):
global count
count = count + n
count = count - n
def run(n):
print("当前⼦线程:{}".format(threading.current_thread().name))
for i in range(10000000):
# lock.acquire()
# try:
change(n)
# finally:
# lock.release()
if__name__ == '__main__':
print("主线程:{}".format(threading.current_thread().name))
thread_1 = threading.Thread(target=run, args=(3,))
thread_2 = threading.Thread(target=run, args=(10,))
thread_1.start()
thread_2.start()
thread_1.join()
thread_2.join()
print(count)
python中的线程需要先获取GIL(Global Interpreter Lock)锁才能继续运⾏,每⼀个进程仅有⼀个GIL,线程在获取到GIL之后执⾏100字节码或者遇到IO中断时才会释放GIL,这样在CPU密集的任务中,即使有多个CPU,多线程也是不能够利⽤多个CPU来提⾼速率,甚⾄可能会因为竞争GIL导致速率慢于单线程。
所以对于CPU密集任务往往使⽤多进程,IO密集任务使⽤多线程。
四、多核并⾏计算
原⽂链接:
1.多进程库 Multiprocessing
import math
import datetime
import multiprocessing as mp
def train_on_parameter(name, param):
result = 0
for num in param:
result += math.sqrt(num * math.tanh(num) / math.log2(num) / math.log10(num))
return {name: result}
if__name__ == '__main__':
start_t = datetime.datetime.now()
num_cores = int(mp.cpu_count())
print("本地计算机有: " + str(num_cores) + " 核⼼")
pool = mp.Pool(num_cores)
param_dict = {'task1': list(range(10, 30000000)),
'task2': list(range(30000000, 60000000)),
'task3': list(range(60000000, 90000000)),
'task4': list(range(90000000, 120000000)),
'task5': list(range(120000000, 150000000)),
'task6': list(range(150000000, 180000000)),
'task7': list(range(180000000, 210000000)),
'task8': list(range(210000000, 240000000))}
results = [pool.apply_async(train_on_parameter, args=(name, param)) for name, param in param_dict.items()]
results = [p.get() for p in results]
end_t = datetime.datetime.now()
elapsed_sec = (end_t - start_t).total_seconds()
print("多进程计算共消耗: " + "{:.2f}".format(elapsed_sec) + " 秒")
核⼼数量: cpu_count() 函数可以获得你的本地运⾏计算机的核⼼数量。
如果你购买的是 Intel i7或者以上版本的芯⽚,你会得到⼀个乘以2的数字,得益于超线程 (Hyper-Threading) 结构,Python 可利⽤核⼼数量是真实数量的2倍!所以我在前⽂中会建议Python开发者购买 i7 ⽽不是第⼋代之前的 i5。
进程池: Pool() 函数创建了⼀个进程池类,⽤来管理多进程的⽣命周期和资源分配。
这⾥进程池传⼊的参数是核⼼数量,意思是最多有多少个进程可以进⾏并⾏运算。
异步调度: apply_async() 是进程池的⼀个调度函数。
第⼀个参数是计算函数,和⾥多线程计算教程⾥创建线程的参数 target 类似。
第
⼆个参数是需要传⼊计算函数的参数,这⾥传⼊了计算函数名字和计算调参。
⽽异步的意义是在调度之后,虽然计算函数开始运⾏并且可能没有结束,异步调度都会返回⼀个临时结果,并且通过列表⽣成器 (参考: ) 临时的保存在⼀个列表⾥,这⾥就是 results。
调度结果: 如果你检查列表 results ⾥的类,你会发现 apply_async() 返回的是 ApplyResult,也就是调度结果类。
这⾥⽤到了 Python 的异步功能,⽬前教程还没有讲到,简单的来说就是⼀个⽤来等待异步结果⽣成完毕的容器。
获取结果: 调度结果 ApplyResult 类可以调⽤函数 get(), 这是⼀个⾮异步函数,也就是说 get() 会等待计算函数处理完毕,并且返回结果。
这⾥的结果就是计算函数的 return。
2.并⾏计算
多线程因为共享⼀个进程的内存,所以在并⾏计算的时候会出现资源竞争的问题,这个在已经提到过。
⽽多进程虽然避免了这个问题,但是⽆法像多线程⼀样轻易的调⽤⼀个内存的资源。
为了能让多进程之间进⾏通讯 (IPC),Python 的 Multiprocessing 库提供了⼏种⽅案: Pipe, Queue 和 Manager。
这⾥ Pipe 我就直接引⽤⼀个外部我觉得很简单明了的介绍,Queue 有兴趣的⼩伙伴可以在教程结尾找到外部的链接,然后我会在之前的例⼦中加⼊ Manager。
管道Pipe:
Pipe可以是单向(half-duplex),也可以是双向(duplex)。
我们通过mutiprocessing.Pipe(duplex=False)创建单向管道 (默认为双向)。
⼀个进程从PIPE⼀端输⼊对象,然后被PIPE另⼀端的进程接收,单向管道只允许管道⼀端的进程输⼊,⽽双向管道则允许从两端输⼊。
import multiprocessing as mul
def proc1(pipe):
pipe.send('hello')
print('proc1 rec:', pipe.recv())
def proc2(pipe):
print('proc2 rec:', pipe.recv())
pipe.send('hello, too')
# Build a pipe
pipe = mul.Pipe()
if__name__ == '__main__':
# Pass an end of the pipe to process 1
p1 = mul.Process(target=proc1, args=(pipe[0],))
# Pass the other end of the pipe to process 2
p2 = mul.Process(target=proc2, args=(pipe[1],))
p1.start()
p2.start()
p1.join()
p2.join()
管理员 Manager
Manager 是⼀个 Multiprocessing 库⾥的类,⽤来创建可以进⾏多进程共享的数据容器,容器种类包括了⼏乎所有 Python ⾃带的数据类。
import math
import datetime
import multiprocessing as mp
def train_on_parameter(name, param, result_dict, result_lock):
result = 0
for num in param:
result += math.sqrt(num * math.tanh(num) / math.log2(num) / math.log10(num))
with result_lock:
result_dict[name] = result
return
if__name__ == '__main__':
start_t = datetime.datetime.now()
num_cores = int(mp.cpu_count())
print("本地计算机有: " + str(num_cores) + " 核⼼")
pool = mp.Pool(num_cores)
param_dict = {'task1': list(range(10, 30000000)),
'task2': list(range(30000000, 60000000)),
'task3': list(range(60000000, 90000000)),
'task4': list(range(90000000, 120000000)),
'task5': list(range(120000000, 150000000)),
'task6': list(range(150000000, 180000000)),
'task7': list(range(180000000, 210000000)),
'task8': list(range(210000000, 240000000))}
manager = mp.Manager()
managed_locker = manager.Lock()
managed_dict = manager.dict()
results = [pool.apply_async(train_on_parameter, args=(name, param, managed_dict, managed_locker)) for name, param in param_dict.items()]
results = [p.get() for p in results]
print(managed_dict)
end_t = datetime.datetime.now()
elapsed_sec = (end_t - start_t).total_seconds()
print("多线程计算共消耗: " + "{:.2f}".format(elapsed_sec) + " 秒")
这⾥我们⽤ Manager 来创建⼀个可以进⾏进程共享的字典类,随后作为第三个参数传⼊计算函数中。
计算函数把计算好的结果保存在字典⾥,⽽不是直接返回。
在并⾏运算结束之后,我们通过 print() 函数来查看字典⾥的结果。
注意这⾥既然出现了可以共享的数据类,我们就要再次通过锁 (Lock) 来避免资源竞争,所以同时通过 Manager 创建了锁 Lock 类,以第四个参数传⼊计算函数,并且⽤ With 语境来锁住共享的字典类。