前言

最近为了提高我的人脸识别项目的效率,我决定使用多线程/进程,在这里谈谈心得。

一、多进程与多线程的区别

多任务可以由多进程完成,也可以由一个进程内的多线程完成。 进程是由若干线程组成的,一个进程至少有一个线程。 由于线程是操作系统直接支持的执行单元,因此,高级语言通常都内置多线程的支持,Python也不例外,并且,Python的线程是真正的Posix Thread,而不是模拟出来的线程。

在这里找到一个关于多进程和多线程的不同之处的一个小栗子:

我们打个比方,假设你不幸正在准备中考,每天晚上需要做语文、数学、英语、物理、化学这5科的作业,每项作业耗时1小时。 如果你先花1小时做语文作业,做完了,再花1小时做数学作业,这样,依次全部做完,一共花5小时,这种方式称为单任务模型,或者批处理任务模型。 假设你打算切换到多任务模型,可以先做1分钟语文,再切换到数学作业,做1分钟,再切换到英语,以此类推,只要切换速度足够快,这种方式就和单核CPU执行多任务是一样的了,以幼儿园小朋友的眼光来看,你就正在同时写5科作业。 但是,切换作业是有代价的,比如从语文切到数学,要先收拾桌子上的语文书本、钢笔(这叫保存现场),然后,打开数学课本、找出圆规直尺(这叫准备新环境),才能开始做数学作业。操作系统在切换进程或者线程时也是一样的,它需要先保存当前执行的现场环境(CPU寄存器状态、内存页等),然后,把新任务的执行环境准备好(恢复上次的寄存器状态,切换内存页等),才能开始执行。这个切换过程虽然很快,但是也需要耗费时间。如果有几千个任务同时进行,操作系统可能就主要忙着切换任务,根本没有多少时间去执行任务了,这种情况最常见的就是硬盘狂响,点窗口无反应,系统处于假死状态。 所以,多任务一旦多到一个限度,就会消耗掉系统所有的资源,结果效率急剧下降,所有任务都做不好。

可见,多线程的本质执行速度与顺序执行并无差别。 在大佬的解释之下,我决定使用多进程,Python的多线程本质上并不能调动多个CPU的内核,相当于单个内核在不断切换线程,实际上测试也发现,时间所差无几。

二、Python下多进程的使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from multiprocessing import  Process

def fun1(name):
print('测试%s多进程' %name)

if __name__ == '__main__':
process_list = []
for i in range(5): #开启5个子进程执行fun1函数
p = Process(target=fun1,args=('Python',)) #实例化进程对象
p.start()
process_list.append(p)

for i in process_list:
p.join()#等待所有进程结束

print('结束测试')

但是在主进程与子进程之间是相对独立,所以我们经常需要使用队列(Queue)_(队列是一种先进先出的数据结构,具体请百度)_来进行进程间通讯。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from multiprocessing import Process,Queue


def fun1(q,i):
print('子进程%s 开始put数据' %i)
q.put('我是%s 通过Queue通信' %i)

if __name__ == '__main__':
q = Queue()

process_list = []
for i in range(3):
p = Process(target=fun1,args=(q,i,)) #注意args里面要把q对象传给我们要执行的方法,这样子进程才能和主进程用Queue来通信
p.start()
process_list.append(p)

for i in process_list:
p.join()

print('主进程获取Queue数据')
print(q.get())
print(q.get())
print(q.get())
print('结束测试')

上面的代码结果可以看到我们主进程中可以通过Queue获取子进程中put的数据,实现进程间的通信。

三、关于Windows下的多进程问题

为什么至今我的人脸识别程序未更新?当然是因为我鸽。就是Windows下的多进程问题难以实现。 查看官方文档:

16.6.3.2. Windows

Since Windows lacks os.fork() it has a few extra restrictions: More picklability

Ensure that all arguments to Process.__init__() are picklable. This means, in particular, that bound or unbound methods cannot be used directly as the target argument on Windows — just define a function and use that instead. Also, if you subclass Process then make sure that instances will be picklable when the Process.start method is called.

Global variables

Bear in mind that if code run in a child process tries to access a global variable, then the value it sees (if any) may not be the same as the value in the parent process at the time that Process.start was called. However, global variables which are just module level constants cause no problems.

Safe importing of main module

Make sure that the main module can be safely imported by a new Python interpreter without causing unintended side effects (such a starting a new process). For example, under Windows running the following module would fail with a RuntimeError:

1
2
3
4
5
6
7
from multiprocessing import Process

def foo():
print 'hello'

p = Process(target=foo)
p.start()

Instead one should protect the “entry point” of the program by using if __name__ == '__main__': as follows:

1
2
3
4
5
6
7
8
9
from multiprocessing import Process, freeze_support

def foo():
print 'hello'

if __name__ == '__main__':
freeze_support()
p = Process(target=foo)
p.start()

(The freeze_support() line can be omitted if the program will be run normally instead of frozen.) This allows the newly spawned Python interpreter to safely import the module and then run the module’s foo() function. Similar restrictions apply if a pool or manager is created in the main module.

简单来说,就是Windows下缺乏UNIX下的fork。 众所周知,查阅资料发现,python 的 Process 对象在执行 start 方法的时候,有三种方式,分别是 spawn, fork, forkserver。其中 unix 系统默认采用的是 fork 方式。而 windows 默认采用 spawn 方式,且缺少后面两种方法。 spawn 方法会开启一个全新的解释器进程,子进程只会从主进程继承那些对运行 process 的 run() 方法有必要的资源。 在 spawn 方法运行时,需要把主模块 __main__ 作为模块 import 进去,这样主模块中的顶层代码都会执行一遍。 最最重要的是,参数的传递无法实现。意味着我将需要使用大量的进程间通讯将主进程中的参数传递到子进程中,这就是问题所在。

三、总结

无论是在Windows还是UNIX中,使用多线程/进程都需要丰富的操作系统知识,否则很有可能陷在一个莫名其妙的错误中无法自拔。