前言
在网上看到一篇帖子,讲的是 "为什么在Python里推荐使用多进程而不是多线程?",大致内容待细细道来,背景是这样的,
1. GIL是什么?
GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。
2. 每个CPU在同一时间只能执行一个线程
在单核CPU下的多线程其实都只是并发,不是并行,并发和并行从宏观上来讲都是同时处理多路请求的概念。但并发和并行又有区别,并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔内发生。
3、在Python多线程下,每个线程的执行方式:
可见,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”,并且在一个python进程中,GIL只有一个。拿不到通行证的线程,就不允许进入CPU执行。
在Python2.x里,GIL的释放逻辑是当前线程遇见IO操作或者ticks计数达到100(ticks可以看作是Python自身的一个计数器,专门做用于GIL,每次释放后归零,这个计数可以通过 sys.setcheckinterval 来调整),进行释放。
而每次释放GIL锁,线程进行锁竞争、切换线程,会消耗资源。并且由于GIL锁存在,python里一个进程永远只能同时执行一个线程(拿到GIL的线程才能执行),这就是为什么在多核CPU上,python的多线程效率并不高。
而在python3.x中,GIL不使用ticks计数,改为使用计时器(执行时间达到阈值后,当前线程释放GIL),这样对CPU密集型程序更加友好,但依然没有解决GIL导致的同一时间只能执行一个线程的问题,所以效率依然不尽如人意。
请注意:多核多线程比单核多线程更差,原因是单核下多线程,每次释放GIL,唤醒的那个线程都能获取到GIL锁,所以能够无缝执行,但多核下,CPU0释放GIL后,其他CPU上的线程都会进行竞争,但GIL可能会马上又被CPU0拿到,导致其他几个CPU上被唤醒后的线程会醒着等待到切换时间后又进入待调度状态,这样会造成线程颠簸(thrashing),导致效率更低。
每个进程有各自独立的GIL,互不干扰,这样就可以真正意义上的并行执行,所以在python中,多进程的执行效率优于多线程(仅仅针对多核CPU而言)。所以在这里得出的结论是:多核下,想做并行提升效率,比较通用的方法是使用多进程,能够有效提高执行效率。
那么,什么是多进程和多线程呢?
一、多进程
1.1 multiprocessing模块
在 Windows 上要实现跨平台的多进程,我们大多使用 multiprocessing 模块。
p = multiprocessing.Process(target=, args=)'''
target 指定的是当进程执行时,需要执行的函数
args 是当进程执行时,需要给函数传入的参数
注意: args必须是一个tuple, 特别是当函数需要传入一个参数时 (1,)
p 代表的是一个多进程,
p.is_alive() 判断进程是否存活
p.run() 启动进程
p.start() 启动进程,他会自动调用run方法,推荐使用start
p.join(timeout) 等待子进程结束或者到超时时间
p.terminate() 强制子进程退出
p.name 进程的名字
p.pid 进程的pid
'''
1.2 创建子进程
multiprocessing 模块提供了一个 Process 类来代表一个进程对象,下面用一个例子展示如何启动一个子进程并等待其结束:
#!/usr/bin/env python# -*- coding:utf-8 -*-
# @Time : 2018/5/19 20:09
# @Author : zhouyuyao
# @File : 2018-05-19.py
import multiprocessing
import os
# 子进程要执行的代码
def run_proc(name):
print('Run child process {0} ({1})...'.format(name, os.getpid()))
def parent_process():
print('Parent process {0}.'.format(os.getpid()))
p = multiprocessing.Process(target=run_proc, args=('test',))
print('Child process will start.')
p.start()
p.join()
print('Child process end.')
if __name__=='__main__':
parent_process()
结果如下,每次运行都会产生不同的进程ID
Parent process 5232.Child process will start.
Run child process test (17912)...
Child process end.
创建子进程时,只需要传入一个执行函数和函数的参数,创建一个Process实例,用 start() 方法启动,这样创建进程比 fork() 还要简单。
join() 方法可以等待子进程结束后再继续往下运行,通常用于进程间的同步。
1.3 启动多个进程
#!/usr/bin/env python# -*- coding:utf-8 -*-
# @Time : 2018/5/20 19:51
# @Author : zhouyuyao
# @File : demon2.py
import multiprocessing
import time
import os
def worker(args, interval): # interval 表示间隔
print("start worker {0}({1})".format(args, os.getpid()))
time.sleep(interval)
print("end worker {0}({1})".format(args, os.getpid()))
def main():
print("start main")
print('Parent process {0}.'.format(os.getpid()))
p1 = multiprocessing.Process(target=worker, args=(1,1))
print('Parent process {0}.'.format(os.getpid()))
p2 = multiprocessing.Process(target=worker, args=(2,2))
print('Parent process {0}.'.format(os.getpid()))
p3 = multiprocessing.Process(target=worker, args=(3,3))
p1.start()
p2.start()
p3.start()
print("end main")
if __name__ == '__main__':
main()
''' 结果
start main
Parent process 6180. # 此处得到的进程ID一样,原因暂不明
Parent process 6180.
Parent process 6180.
end main
start worker 1(17596)
start worker 2(5872)
start worker 3(19228)
end worker 1(17596)
end worker 2(5872)
end worker 3(19228)
'''
1.4 判断子进程是否存在
我们也可以获取运行代码机器的CPU核数,以及查看哪个进程的子进程正在运行,如下所示
#!/usr/bin/env python# -*- coding:utf-8 -*-
# @Time : 2018/5/20 19:51
# @Author : zhouyuyao
# @File : demon2.py
import multiprocessing
import time
import os
def worker(args, interval): # interval 表示间隔
print("start worker {0}({1})".format(args, os.getpid()))
time.sleep(interval)
print("end worker {0}({1})".format(args, os.getpid()))
def main():
print("start main")
print('Parent process {0}.'.format(os.getpid()))
p1 = multiprocessing.Process(target=worker, args=(1,1))
print('Parent process {0}.'.format(os.getpid()))
p2 = multiprocessing.Process(target=worker, args=(2,2))
print('Parent process {0}.'.format(os.getpid()))
p3 = multiprocessing.Process(target=worker, args=(3,3))
p1.start()
p1.join(timeout=0.5)
p2.start()
p3.start()
print("the number of CPU is: {0}".format(multiprocessing.cpu_count()))
'''cpu_count(self):Returns the number of CPUs in the system'''
for p in multiprocessing.active_children():
"""active_children :rtype: list[multiprocessing.Process]"""
print("The name of active children is: {0}, pid is: {1} is alive".format(p.name, p.pid))
print("end main")
if __name__ == '__main__':
main()
运行之后我们得到的结果如下
start mainParent process 22560.
Parent process 22560.
Parent process 22560.
start worker 1(20788)
the number of CPU is: 4
The name of active children is: Process-3, pid is: 23508 is alive
The name of active children is: Process-2, pid is: 6440 is alive
The name of active children is: Process-1, pid is: 20788 is alive
end main
start worker 2(6440)
start worker 3(23508)
end worker 1(20788)
end worker 2(6440)
end worker 3(23508)
1.5 进程同步(进程锁Lock)
对于一些互斥的资源来说,进程间需要进程互斥来访问。否则导致资源访问受阻,或者最后的结果混乱等情况。对于标准输出这个资源来说,如果多个资源同属输出信息,可能会导致输出的信息混乱。所以需要使用锁来避免资源互斥访问。
当多个进程需要访问共享资源的时候,Lock可以用来避免访问的冲突。
#!/usr/bin/env python# -*- coding:utf-8 -*-
# @Time : 2018/5/20 21:31
# @Author : zhouyuyao
# @File : demon4.py
import multiprocessing
def worker_with(lock, f):
with lock:
with open('file.txt',"a+") as fs:
fs.write('Lock acquired via with\n')
# fs.close()
def worker_no_with(lock, f):
lock.acquire()
'''
This method blocks until the lock is unlocked, then sets it to
locked and returns True.
'''
try:
# fs = open(f, "a+")
with open('file.txt',"a+") as fs:
fs.write('Lock acquired directly\n')
finally:
lock.release()
"""Release a lock.
When the lock is locked, reset it to unlocked, and return.
If any other coroutines are blocked waiting for the lock to become
unlocked, allow exactly one of them to proceed.
When invoked on an unlocked lock, a RuntimeError is raised.
There is no return value.
"""
def main():
f = "file.txt"
lock = multiprocessing.Lock()
w = multiprocessing.Process(target=worker_with, args=(lock, f))
nw = multiprocessing.Process(target=worker_no_with, args=(lock, f))
w.start() # p.start() 启动进程,他会自动调用run方法,推荐使用start
nw.start()
w.join() # p.join(timeout) 等待子进程结束或者到超时时间
'''
join()代表启动多进程,但是阻塞并发运行,一个进程执行结束后再执行第二个进程。
可以给其设置一个timeout值比如 join(5)代表5秒后无论当前进程是否结果都继续并发执行第二个进程。
'''
nw.join()
if __name__ == "__main__":
main()
结果会产生一个file.txt文件,内容如下(以下为运行多次的结果)
1.6 进程间共享数据
几个进程之间的都拥有自己独立的命名空间和地址空间,无法通过一些全局变量来实现,multiprocessing提供了一些特殊的函数来实现共享变量。
进程间共享变量(Value、Array、Manager)
Value、Array 是通过共享内存的方式共享数据 ,Manager 则是通过共享进程的方式共享数据
1)Value、Array
#!/usr/bin/env python# -*- coding:utf-8 -*-
# @Time : 2018/5/20 23:10
# @Author : zhouyuyao
# @File : demon7.py
from multiprocessing import Process,Value,Array
def f(n,a):
n.value = 3.1415926
for i in range(len(a)):
a[i] = -a[i]
def main():
num = Value('d', 0.0)
arr = Array('i', range(10))
''' Value() 和 Array() 都有两个参数第一个参数代表存放的值的类型,第二个参数代表其值 '''
p = Process(target=f, args=(num, arr))
p.start()
p.join()
print(num.value)
print(arr[:])
if __name__ == "__main__":
main()
''' 结果
3.1415926
[0, -1, -2, -3, -4, -5, -6, -7, -8, -9]
'''
2)manager
Manager管理的共享数据类型有:Value、Array、dict、list、Lock、Semaphore 等等,同时 Manager 还可以共享类的实例对象。
这个方式支持的类型更多,灵活性更大,但是速度要慢于Value,Array。
#!/usr/bin/env python# -*- coding:utf-8 -*-
# @Time : 2018/5/20 23:13
# @Author : zhouyuyao
# @File : demon8.py
from multiprocessing import Process,Manager
def f(d,l):
d[1] = '1'
d['2'] = 2
d[0.25] = None
l.reverse()
'''reverse() 函数用于反向列表中元素。
该方法没有返回值,但是会对列表的元素进行反向排序'''
def main():
manager = Manager()
d = manager.dict()
l = manager.list(range(10))
p = Process(target=f, args=(d, l))
p.start()
p.join()
print(d)
print(l)
if __name__ == "__main__":
main()
''' 结果
{1: '1', '2': 2, 0.25: None}
[9, 8, 7, 6, 5, 4, 3, 2, 1, 0]
'''
参考资料
1. http://bbs.51cto.com/thread-1349105-1.html 为什么在 Python 里推荐使用多进程而不是多线程?
2. https://www.liaoxuefeng.com/wiki/0014316089557264a6b348958f449949df42a6d3a2e542c000/001431927781401bb47ccf187b24c3b955157bb12c5882d000
浅谈 python multiprocessing(多进程)下如何共享变量
http://blog.51cto.com/forlinux/1423390 python之多进程multiprocessing