上一次写到了python多进程的map方法的应用(传送门),但是后续应用的过程中发现多进进程程在应用过程中的一些进程调用和执行的小技巧,那么我们用代码来看一下具体现象。

from multiprocessing import Pool
import os
import time
x, y, z, k = 1,2,0,0
print("start",os.getpid())
# test the parallel object copy
def add(a, b, c):
time.sleep(5)
c = a + b
print(os.getpid(), id(c))
k = [c]
print(k, id(k))
return c
if __name__ == '__main__':
tries = 3
p = Pool(tries)
result = p.starmap(add, zip([x + i for i in range(tries)], [y] * tries, [z] * tries))
print(result, id(k), k)
print('-----')
print(os.getpid(), add(x, y, z))

运行结果为:

start 1536
start 16740
start 13772
start 8272
13772 1928102064
16740 1928102032
8272 1928102096
[4] 46414792
[3] 37829576
[5] 46283720
[3, 4, 5] 1928101936 0
-----
1536 1928102032
[3] 47449288
1536 3

可以看到在我们一共用在进程池中创建了3个进程,他们的id分别是13772,8272,16740,加上主进程1536,我们一共运行了4个进程。并且在`add`函数中也是可以看到顺序的执行着这几个进程。最后我们执行完分进程后,再次回到主进程再次运行了add函数。

不过值得注意的是,在执行分进程函数前, print("start",os.getpid()) 是在 add 函数外的一段代码,应该只在主进程中执行,为什么还会打印出分进程的id呢?

答案就是在我们在用multiprocess时要在最后的主进程入口加入if __name__ =='__main__':这段代码,在这段代码之前的代码都不是作为主程序入口的,因此在创建主进程的时候所以的文件都被加载一遍,因此才会打印出分进程的id。

如果有需要只在主进程中执行代码,一定要安排在主程序的代码入口以后。

另外还有一个现象,cpu在调用进程的时候是根据进程是否空闲来决定的,在用pool来创建进程池时,是否创建新的进程是根据当时cpu的空闲进程数决定的,比如我们去掉add函数中的time.sleep(5)得到的结果就是这样的:

start 13060
start 13280
start 2620
13280 1402661392
[3] 1921044238216
13280 1402661424
[4] 1921044238216
13280 1402661456
[5] 1921044238216
[3, 4, 5] 1402661296 0
-----
13060 1402661392
[3] 2930835517000
13060

可以看到虽然在创建了几个子进程,但是由于程序执行的速度特别快,所以实际上只有一个进程被调用。

而当Pool中的进程数量少于任务时,比如这里设置traies=3, 但是只申请2个进程p=Pool(2),可以看到的现象是函数体外的代码只被了2次。由此可以知道,每个子进程都是在初始化时同主进程一样加载一下函数体外的代码,但是一旦建立好进程,那么多个进程是统过循环的方式对函数传入参数来进行计算,并最后返回结果。

start 10144
start 5360
start 8668
5360 1928102032
8668 1928102064
[3] 46414792
[4] 46480328
5360 1928102096
[5] 47142984
[3, 4, 5] 1928101936 0
-----
10144 1928102032
[3] 47383688
10144 3

可以看到在存在sleep函数时,进程是2个一组运行的,这两个运行完成后,下一次循环才开始(当任务不够分配的时候是默认按照进程顺序分配的) 。

Case Study:我们知道函数体外的代码都是要在分进程中执行一遍的,所以这里面生成的数据在内存都是独立的,而不是共享的,其实这对于一些全局变量来说是一种浪费内存的行为。当这些变量占用空间很小可以不用特别在意,但是当我们遇到某一段代码的运行需要很大的空间,这时如果机器内存不够就容易造成内存溢出错误,那么除了用官方提供的Array等共享数组外,还可以想办法只让这部分代码和数据在主进程运行。下面提供一个问题的解决思路:

from multiprocessing import Pool
import os
import time
import pickle
x, y, z, k = 1,2,0,0
print("start",os.getpid())
GenerateData = False
try:
with open('x.dat', 'rb') as file:
p = pickle.load(file)
print(p)
except FileNotFoundError:
GenerateData = True
# test the parallel object copy
def add(a, b, c):
# time.sleep(5)
c = a + b
print(os.getpid(), id(c))
k = [c]
print(k, id(k))
return c
if __name__ == '__main__':
if GenerateData:
if os.path.exists('x.dat'):
os.remove('x.dat')
with open('x.dat', 'wb') as file:
pickle.dump(1, file)
tries = 3
p = Pool(tries)
result = p.starmap(add, zip([x + i for i in range(tries)], [y] * tries, [z] * tries))
print(result, id(k), k)
print('-----')
print(os.getpid(), add(x, y, z))

运行结果:

start 16648
3
start 12944
1
start 13564
1
start 7792
1
12944 1928102032
[3] 46545864
12944 1928102064
[4] 47274056
12944 1928102096
[5] 47707144
[3, 4, 5] 1928101936 0
-----
16648 1928102032
[3] 47384008
16648 3

这里通过设定一个GenerateData变量来控制是否生成并存储数据, 再通过try的错误处理机制处理当空文件时的情况。基本实现了我们想要的逻辑。

一点经验,欢迎讨论。

以上这些问题都是在windows进行的测试,由于windows和linux底层对于multiprocessing的实现不同,所以表现也不相同。具体说来,Linux是通过fork方式来将主进程的变量直接copy一份过来用,而windows不能用fork方法,因此是直接在子进程重新运行一遍程序,相当于import。所以上面的例子会出现如下的结果:

start 1536
13772 1928102064
16740 1928102032
8272 1928102096
[4] 46414792
[3] 37829576
[5] 46283720
[3, 4, 5] 1928101936 0
-----
1536 1928102032
[3] 47449288
1536 3

可以看到主进程中 print("start",os.getpid()) 只执行了一次,子进程并没有重复执行命令。