Python3进程与线程(v3.7)

[TOC]

多进程

Unix-like 进程

Unix/Linux操作系统提供了一个fork()系统调用,与普通函数不同,该函数调用一次返回两次。因为操作系统把当前进程(父进程)复制一份(子进程),然后分别在父进程和子进程返回。子进程永远返回0,父进程返回子进程的ID。Python在os模块封装了常见的系统调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
import os
print('Process (%s) starting...' % os.getpid())

pid = os.fork()
if pid == 0 :
print('child process (%s) , parent process is (%s) ' % (os.getpid(),os.getppid()))
else :
print('(%s) just create child process (%s) ' % (os.getpid(),pid))

执行结果:
Process (51481) starting...
(51481) just create child process (51482)
child process (51482) , parent process is (51481)

跨平台进程

Windows系统没有fork调用,想要在Window平台实现需要使用跨平台版本多进程模块multiprocessing,该模块提供了一个Process进程对象。

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

def run_proc(name):
print('Run child process %s (%s)...' % (name, os.getpid()))


if __name__ == '__main__':
print('Parent process (%s)' % os.getpid())
p = Process(target=run_proc, args=('test',))
print('Child process will start.')
p.start()
p.join()
print('Child process end.')

执行结果:
Parent process (61595)
Child process will start.
Run child process test (61596)...
Child process end.

创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用start()方法启动,join()方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。

进程池Pool

当要启用大量子进程时,可以使用进程池指创建子进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import os,time,random
from multiprocessing import Pool

def long_time_task(name):
print('Run task %s (%s)...' % (name, os.getpid()))
start = time.time()
time.sleep(random.random() * 3)
end = time.time()
print('Task %s runs %0.2f seconds.' % (name, (end - start)))

if __name__ == '__main__':
print('Parent process %s.' % os.getpid())
p = Pool(4)
for i in range(5):
p.apply_async(long_time_task, args=(i,))

print('Waiting for all subprocesses done...')
p.close()
p.join()
print('All subprocesses done.')


执行结果:
Parent process 71982.
Waiting for all subprocesses done...
Run task 0 (71983)...
Run task 1 (71984)...
Run task 2 (71985)...
Run task 3 (71986)...
Task 1 runs 0.89 seconds.
Run task 4 (71984)...
Task 2 runs 1.02 seconds.
Task 3 runs 1.86 seconds.
Task 0 runs 1.95 seconds.
Task 4 runs 1.12 seconds.
All subprocesses done.

Pool对象调用join()方法会等待所有子进程执行完毕,调用join()之前必须先调用close(),调用close()之后就不能继续添加新的Process。 

注意:上面先执行完task0123而task4在前面某个task执行完毕才执行,这是因为前面Pool大小设置为4,因此最多同时执行4个进程,这是Pool有意设计的限制,并不是操作系统的限制。Pool的默认大小是CPU核数。

多线程

Python的线程是真正的Posix Thread,而不是模拟出来的线程。
Python的标准库提供了两个模块:_threadthreading,_thread是低级模块,threading是高级模块,对_thread进行了封装。绝大多数情况下,我们只需要使用threading这个高级模块。

启动一个线程就是把一个函数传入Thread并创建实例实例,然后调用start()方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import threading, time

def func():
g = (x for x in range(5))

for n in g:
print('thread %s is running %s' % (threading.current_thread().getName(), n))
time.sleep(2)
print('thread %s ended' % threading.current_thread().getName())

print('thread %s is running...' % threading.current_thread().getName())
t = threading.Thread(target=func, name='FuncThread')
t.start() #开始一个线程
t.join() #同步FuncThread线程
print('thread %s ended.' % threading.current_thread().name)

线程锁

多线程和多进程的最大不同之处在于,多进程中同一变量各自有一拷贝存在每个进程中互不影响。而多线程中所有变量由所有线程共享。因此,线程之间共享数据最大的危险在于多个线程同时改一个变量。为避免这种情况,Python引入线程锁threading.Lock()

1
2
3
4
5
6
7
8
9
10
11
12
lock = threading.Lock()

def run_thread(n):
for i in range(100000):
# 获取锁
lock.acquire()
try:
# 修改变量
...
finally:
# 释放锁
lock.release()

当多个线程同时执行lock.acquire()时,只有一个线程能成功地获取锁,然后继续执行代码,其他线程就继续等待直到获得锁为止。获得锁的线程用完后一定要释放锁,否则那些苦苦等待锁的线程将永远等待下去,成为死线程。所以我们用try…finally来确保锁一定会被释放。

多核CPU

正常情况多核CPU可以同时执行多个线程,要想把N核CPU跑满,必须启动N个死循环线程。

1
2
3
4
5
6
7
8
9
10
import threading, multiprocessing

def loop():
x = 0
while True:
x = x ^ 1

for i in range(multiprocessing.cpu_count()):
t = threading.Thread(target=loop)
t.start()

上面代码在四核CPU上启动,监控发现只占用了100%左右的CPU也就是只使用了1核,而不是使用400%的CPU,因为Python解释器执行代码时有一个GIL锁(Global Interpreter Lock),任何线程执行前必须先获取GIL锁,然后每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。

Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。

ThreadLocal

ThreadLocal是一个全局变量,但每个线程都只能读写自己线程的独立副本互不干扰,并且通过ThreadLocal变量不用管理锁的问题,ThreadLocal内部会自己处理。

1
2
3
4
5
6
7
import threading

# 创建全局ThreadLocal对象
tl = threading.local()

#在线程内对tl.变量名进行读取和赋值
...代码省略

进程与线程比较

要实现多任务,通常我们会设计Master-Worker模式,Master负责分配任务,Worker负责执行任务。

如果用多进程实现Master-Worker,主进程就是Master,其他进程就是Worker。
如果用多线程实现Master-Worker,主线程就是Master,其他线程就是Worker。

多进程模式优点:稳定性高,一个子进程崩溃了,不会影响主进程和其他子进程。(主进程挂了所有进程就全挂了,但是Master进程只负责分配任务,挂掉的概率低)。
多进程模式缺点:创建进程的代价大,在Unix/Linux系统下,用fork调用还行,在Windows下创建进程开销巨大。另外,操作系统能同时运行的进程数也是有限的。

多线程模式通常比多进程快一点,但是也快不到哪去,多线程模式致命的缺点是任何一个线程挂掉都可能直接造成整个进程崩溃,因为所有线程共享进程的内存。

在Windows下,多线程的效率比多进程要高,所以微软的IIS服务器默认采用多线程模式。由于多线程存在稳定性的问题,IIS的稳定性就不如Apache。为了缓解这个问题,IIS和Apache现在又有多进程+多线程的混合模式。

分布式进程

暂无记录