一.引言
Java 开发中常用到多线程和线程池提高程序运行效率和机器利用率,Python 多线程用到了 Parallel 类 和 Multiprocessing 类,除此之外还有 _thread,threading 等很多线程相关的类,可以配合 os,sys,subprocess 等工具类实现复杂的操作。下面的 Demo 通过 sum 求和的例子介绍几种多线程实现方法。
二.Parallel 无 Lock
joblib 库下面的 Parallel 实现了并行提高效率的功能,delayed 处执行多线程要执行的函数,njobs 指定并行的 core 数,delayed 最后面的括号负责传递函数 add_sum 使用的参数。
#!/usr/bin/python
# -*- coding: UTF-8 -*-
from joblib import Parallel, delayed
import numpy as np
def add_sum(_numList):
return sum(_numList)
if __name__ == '__main__':
numList = [[1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7]]
cores = 4
results = Parallel(n_jobs=cores)(delayed(add_sum)(num) for num in numList)
print(sum(results))
三.MultiProcessing 无 Lock
Java 中常用到线程池,主要使用 ThreadPoolExecutor 类开发,Python 借助 multiprocessing 也可以实现线程池。
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import multiprocessing
def add_sum(nums):
return sum(nums)
if __name__ == '__main__':
pool = multiprocessing.Pool(processes=2)
numLists = [[1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7]]
sub_process = []
for i in range(len(numLists)):
data = numLists[i]
process = pool.apply_async(add_sum, (data,))
sub_process.append(process)
pool.close()
pool.join()
result = 0
for process in sub_process:
result += process.get()
print(result)
Tips
-> .Pool 方法可以实现线程池,processes 代表线程池的并行度
-> apply_async 实现异步执行提交,其中如果只传一个参数,需要在参数后加一个逗号,否则函数会识别错误
-> .close 关闭线程池,.join 方法等待线程池中所有任务执行完毕开始执行下面的代码,所以要避免写死循环和 Bug
-> .get 可以拿到线程处理结束后返回的结果,需要自己将多个线程的结果聚合
四.MultiProcessing 有 Lock
多线程除了并行执行外,还经常设计到同步的问题,java中通过 synchronized 加锁,控制对统一内存变量的读写, multiprocessing 通过 lock 类实现加锁解决线程同步的问题。
#!/usr/bin/python
# -*- coding: UTF-8 -*-
import multiprocessing
import time
import numpy as np
import sys
def add_sum(_initial, _numList, _lock):
while True:
try:
_lock.acquire()
value = _numList.__next__()
_initial.value = _initial.value + value
_lock.release()
except StopIteration:
_lock.release()
sys.exit()
if __name__ == "__main__":
lock = multiprocessing.Lock()
initial = multiprocessing.Value('i', 0)
work_nums = 4
sub_process = [] # 处理数据进程集合
numList = [[1, 2, 3, 4], [2, 3, 4, 5], [3, 4, 5, 6], [4, 5, 6, 7]]
st = time.time()
for i in range(work_nums):
sub_process_tmp = multiprocessing.Process(target=add_sum, args=(initial, iter(numList[i]), lock))
sub_process.append(sub_process_tmp)
for process in sub_process:
process.start()
for process in sub_process:
process.join()
end = time.time()
# 结果对比
print("Add Sum Cost :", (end - st))
realSum = np.array(numList).sum()
print("Real Result: ", realSum, " Multi Process Result: ", initial.value)
Tips
-> lock = multiprocessing.Lock() 获得程序运行的锁,可以在线程中使用
-> initial = multiprocessing.Value('i', 0) 代表全局变量,类似于 spark 的 longAccumulator ,可以通过 .value 方法获取其值,'i' 代表类型,一般有以下类型:
Type | C Type | Python Type | Minimum bytes |
---|---|---|---|
c | char | char | 1 |
b | signed char | int | 1 |
B | unsigned char | int | 1 |
u | unicode | unicode char | 2 |
h | signed short | int | 2 |
H | unsigned short | int | 2 |
i | signed int | int | 2 |
I | unsigned int | long | 2 |
l | signed long | int | 4 |
L | unsigned long | long | 4 |
f | float | float | 4 |
d | double | float | 8 |
如果共享字符串,可以执行下述代码,将 c_char_p 作为共享变量 Value 的类型指示,具体可以查看 ctypes,由于常见的是数值型累加,这里不多赘述
from ctypes import c_char_p
-> target 为多线程执行的目标函数,args 为目标函数对应的参数,可以通过参数平均分配任务到每个线程里
-> start 启动线程,类似于 java 的 Runnable 类,join 等待线程运行结束
-> lock 有 acquire() 获取 和 release() 释放方法,同步的逻辑可以放在二者之间保证结果的一致性
-> excpet 中记得添加 lock.release() 方法,否则会导致锁无法释放程序卡死
无锁运行结果:
Add Sum Cost : 0.1731090545654297
Real Result: 64 Multi Process Result: 39
有锁运行结果:
Add Sum Cost : 0.18215608596801758
Real Result: 64 Multi Process Result: 64
无锁状态下程序运行结果不一致,可能是39,可能是正确结果 64,也可以是其他数值,加锁后数值一致,但是耗时会增加。
五.总结
通过数组求和的简单 demo,实现了有锁并行和无锁并行多种方案,可以根据自己需求修改上述代码,简单的并行用 Parallel 实现比较便捷,复杂的实现可以使用 multiprocessing,更具体的参数和实现细节可以参考官方 Api 解释。