概要
由于python中的GIL(全局解释器锁)的存在,也就是多线程的时候,同一时间只能有一个线程在CPU上运行,而且是单个CPU上运行,不管你的CPU有多少核数。如果想要充分地使用多核CPU的资源,在python中大部分情况需要使用多进程。
多进程
Python中的多进程是通过multiprocessing
包来实现的,和多线程的threading.Thread
差不多,它可以利用multiprocessing.Process
对象来创建一个进程对象。这个进程对象的方法和线程对象的方法差不多也有start(), run(), join()
等方法,其中有一个方法不同Thread
线程对象中的守护线程方法是setDeamon
,而Process
进程对象的守护进程是通过设置daemon
属性来完成的。
Python多进程实现方法一
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('结束测试')
结果
测试Python多进程
测试Python多进程
测试Python多进程
测试Python多进程
测试Python多进程
结束测试
Process finished with exit code 0
上面的代码开启了5个子进程去执行函数,我们可以观察结果,是同时打印的,这里实现了真正的并行操作,就是多个CPU同时执行任务。我们知道进程是python中最小的资源分配单元,也就是进程中间的数据,内存是不共享的,每启动一个进程,都要独立分配资源和拷贝访问的数据,所以进程的启动和销毁的代价是比较大了,所以在实际中使用多进程,要根据服务器的配置来设定。
Python多进程实现方法二
通过类继承的方法来实现
from multiprocessing import Process
class MyProcess(Process): #继承Process类
def __init__(self,name):
super(MyProcess,self).__init__()
self.name = name
def run(self):
print('测试%s多进程' % self.name)
if __name__ == '__main__':
process_list = []
for i in range(5): #开启5个子进程执行fun1函数
p = MyProcess('Python') #实例化进程对象
p.start()
process_list.append(p)
for i in process_list:
p.join()
print('结束测试')
结果
测试Python多进程
测试Python多进程
测试Python多进程
测试Python多进程
测试Python多进程
结束测试
Process finished with exit code 0
Process类的其他方法
构造方法:
Process([group [, target [, name [, args [, kwargs]]]]])
group: 线程组
target: 要执行的方法
name: 进程名
args/kwargs: 要传入方法的参数
实例方法:
is_alive():返回进程是否在运行,bool类型。
join([timeout]):阻塞当前上下文环境的进程,直到调用此方法的进程终止或到达指定的timeout(可选参数)。
start():进程准备就绪,等待CPU调度
run():strat()调用run方法,如果实例进程时未指定传入target,这时start默认执行run()方法。
terminate():不管任务是否完成,立即停止工作进程
属性:
daemon:和线程的setDeamon功能一样
name:进程名字
pid:进程号
进程通信
进程是系统独立调度核分配系统资源(CPU、内存)的基本单位,进程之间是相互独立的,每启动一个新的进程相当于把数据进行了一次克隆,子进程里的数据修改无法影响到主进程中的数据,不同子进程之间的数据也不能共享,这是多进程在使用中与多线程最明显的区别。但是难道Python多进程中间难道就是孤立的吗?当然不是,python也提供了多种方法实现了多进程中间的通信和数据共享(可以修改一份数据)
- 进程对列Queue:在python多进程中,它其实就是进程之间的数据管道,实现进程通信。
- 管道Pipe:管道Pipe和Queue的作用大致差不多,也是实现进程间的通信
- Managers:Queue和Pipe只是实现了数据交互,并没实现数据共享,即一个进程去更改另一个进程的数据。那么就要用到Managers
- 进程池:进程池内部维护一个进程序列,当使用时,则去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。就是固定有几个进程可以使用。
多线程
python的多线程是通过threading
模块的Thread
类来实现的。
import threading
import time
def say(name):
print('你好%s at %s' %(name,time.ctime()))
time.sleep(2)
print("结束%s at %s" %(name,time.ctime()))
def listen(name):
print('你好%s at %s' % (name,time.ctime()))
time.sleep(4)
print("结束%s at %s" % (name,time.ctime()))
if __name__ == '__main__':
t1 = threading.Thread(target=say,args=('tony',)) #Thread是一个类,实例化产生t1对象,这里就是创建了一个线程对象t1
t1.start() #线程执行
t2 = threading.Thread(target=listen, args=('simon',)) #这里就是创建了一个线程对象t2
t2.start()
print("程序结束=====================")
- 创建线程对象
t1 = threading.Thread(target=say,args=('tony',))
#Thread
是一个类,实例化产生t1
对象,这里就是创建了一个线程对象t1
- 启动线程
t1.start()
#线程执行
结果:
你好tony at Thu Apr 25 16:46:22 2019
你好simon at Thu Apr 25 16:46:22 2019
程序结束=====================
结束tony at Thu Apr 25 16:46:24 2019
结束simon at Thu Apr 25 16:46:26 2019
Process finished with exit code 0
你好tony at Thu Apr 25 16:46:22 2019 --t1线程执行
你好simon at Thu Apr 25 16:46:22 2019 --t2线程执行
程序结束===================== --主线程执行
结束tony at Thu Apr 25 16:46:24 2019 --sleep之后,t1线程执行
结束simon at Thu Apr 25 16:46:26 2019 --sleep之后,t2线程执行
Process finished with exit code 0 --主线程结束
我们可以看到主线程的print并不是等t1,t2线程都执行完毕之后才打印的,这是因为主线程和t1,t2 线程是同时跑的。但是主进程要等非守护子线程结束之后,主线程才会退出。
上面其实就是python多线程的最简单用法,但是可能有人会和我有一样的需求,一般开发中,我们需要主线程的print打印是在最后面的,表明所有流程都结束了,也就是主线程结束了。这里就引入了一个join
的概念。
import threading
import time
def say(name):
print('你好%s at %s' %(name,time.ctime()))
time.sleep(2)
print("结束%s at %s" %(name,time.ctime()))
def listen(name):
print('你好%s at %s' % (name,time.ctime()))
time.sleep(4)
print("结束%s at %s" % (name,time.ctime()))
if __name__ == '__main__':
t1 = threading.Thread(target=say,args=('tony',)) #Thread是一个类,实例化产生t1对象,这里就是创建了一个线程对象t1
t1.start() #线程执行
t2 = threading.Thread(target=listen, args=('simon',)) #这里就是创建了一个线程对象t2
t2.start()
t1.join() #join等t1子线程结束,主线程打印并且结束
t2.join() #join等t2子线程结束,主线程打印并且结束
print("程序结束=====================")
结果:
你好tony at Thu Apr 25 16:57:32 2019
你好simon at Thu Apr 25 16:57:32 2019
结束tony at Thu Apr 25 16:57:34 2019
结束simon at Thu Apr 25 16:57:36 2019
程序结束=====================
上面代码中加入join方法后实现了,我们上面所想要的结果,主线程print最后执行,并且主线程退出,注意主线程执行了打印操作和主线程结束不是一个概念
,如果子线程不加join,则主线程也会执行打印,但是主线程不会结束,还是需要待非守护子线程结束之后,主线程才结束。
上面的情况,主进程都需要等待非守护子线程结束之后,主线程才结束。那我们是不是注意到一点,我说的是“非守护子线程”
,那什么是非守护子线程?默认的子线程都是主线程的非守护子线程,但是有时候我们有需求,当主进程结束,不管子线程有没有结束,子线程都要跟随主线程一起退出,这时候我们引入一个“守护线程”
的概念。
如果某个子线程设置为守护线程,主线程其实就不用管这个子线程了,当所有其他非守护线程结束,主线程就会退出,而守护线程将和主线程一起退出,**守护主线程,这就是守护线程的意思**
# 设置t2为守护线程
import threading
import time
def say(name):
print('你好%s at %s' %(name,time.ctime()))
time.sleep(2)
print("结束%s at %s" %(name,time.ctime()))
def listen(name):
print('你好%s at %s' % (name,time.ctime()))
time.sleep(4)
print("结束%s at %s" % (name,time.ctime()))
if __name__ == '__main__':
t1 = threading.Thread(target=say,args=('tony',)) #Thread是一个类,实例化产生t1对象,这里就是创建了一个线程对象t1
t1.start() #线程执行
t2 = threading.Thread(target=listen, args=('simon',)) #这里就是创建了一个线程对象t2
t2.setDaemon(True) # 设置t2为守护进程
t2.start()
print("程序结束=====================")
结果:
你好tony at Thu Apr 25 17:15:36 2019
你好simon at Thu Apr 25 17:15:36 2019
程序结束=====================
结束tony at Thu Apr 25 17:15:38 2019
这里设置t2为Daemon,那么主线程就不管t2的运行状态,只管等待t1结束,t1结束之后,主进程就结束,因为t2的时间4秒,t1的时间2秒, 所以主进程结束的时候t2还没结束.
进程主要方法:
join()
:在子线程完成运行之前,这个子线程的父线程将一直被阻塞。setDaemon(True)
:将线程声明为守护线程,必须在start() 方法调用之前设置, 如果不设置为守护线程程序会被无限挂起。这个方法基本和join是相反的。
当我们在程序运行中,执行一个主线程,如果主线程又创建一个子线程,主线程和子线程 就分兵两路,分别运行,那么当主线程完成想退出时,会检验子线程是否完成。如 果子线程未完成,则主线程会等待子线程完成后再退出。但是有时候我们需要的是 只要主线程完成了,不管子线程是否完成,都要和主线程一起退出,这时就可以 用setDaemon方法啦
其他方法:
- run(): 线程被cpu调度后自动执行线程对象的run方法
- start():启动线程活动。
- isAlive(): 返回线程是否活动的。
- getName(): 返回线程名。
- setName(): 设置线程名。
threading模块提供的一些方法:
- threading.currentThread(): 返回当前的线程变量。
- threading.enumerate(): 返回一个包含正在运行的线程的list。正在运行指线程启动后、结束前,不包括启动前和终止后的线程。
- threading.activeCount():返回正在运行的线程数量,与len(threading.enumerate())有相同的结果。
上面的例子中我们注意到两如果个任务如果顺序执行要6s结束,如果是多线程执行4S结束,性能是有所提升的,但是我们要知道这里的性能提升实际上是由于cpu并发实现性能提升,也就是cpu线程切换(多道技术)带来的,而并不是真正的多cpu并行执行。
并行和并发
上面提到了并行和并发,那这两者有什么区别呢?
- 并发:是指一个系统具有处理多个任务的能力(cpu切换,多道技术)
- 并行:是指一个系统具有同时处理多个任务的能力(cpu同时处理多个任务)
并行是并发的一种情况,子集
Python中的多线程是假的多线程!
那为什么python在 多线程中为什么不能实现真正的并行操作呢? 就是在多cpu中执行不同的线程(我们知道JAVA中多个线程可以在不同的cpu中,实现并行运行)这就要提到python中大名鼎鼎GIL,那什么是GIL?
全局解释器锁(GIL)
Python代码的执行由Python虚拟机(解释器)来控制。Python在设计之初就考虑要在主循环中,同时只有一个线程在执行,就像单CPU的系统中运行多个进程那样,内存中可以存放多个程序,但任意时刻,只有一个程序在CPU中运行。同样地,虽然Python解释器可以运行多个线程,只有一个线程在解释器中运行。
对Python虚拟机的访问由 全局解释器锁(GIL) 来控制,正是这个锁能保证同时只有一个线程在运行。在多线程环境中,Python虚拟机按照以下方式执行。
- 设置GIL。
- 切换到一个线程去执行。
- 运行。
- 把线程设置为睡眠状态。
- 解锁GIL。
- 再次重复以上步骤。
对所有面向I/O的(会调用内建的操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其他线程在这个线程等待I/O的时候运行。如果某线程并未使用很多I/O操作,它会在自己的时间片内一直占用处理器和GIL。也就是说,I/O密集型的Python程序比计算密集型的Python程序更能充分利用多线程的好处。
比方我有一个4核的CPU,那么这样一来,在单位时间内每个核只能跑一个线程,然后时间片轮转切换。但是Python不一样,它不管你有几个核,单位时间多个核只能跑一个线程,然后时间片轮转。 看起来很不可思议?但是这就是GIL搞的鬼。任何Python线程执行前,必须先获得GIL锁,然后,每执行100条字节码,解释器就自动释放GIL锁,让别的线程有机会执行。这个GIL全局锁实际上把所有线程的执行代码都给上了锁,所以,多线程在Python中只能交替执行,即使100个线程跑在100核CPU上,也只能用到1个核。通常我们用的解释器是官方实现的CPython,要真正利用多核,除非重写一个不带GIL的解释器。
在Python中利用多核?
多进程算是一种解决方案,还有一种就是调用C语言的链接库。对所有面向I/O的(会调用内建的操作系统C代码的)程序来说,GIL会在这个I/O调用之前被释放,以允许其他线程在这个线程等待I/O的时候运行。我们可以把一些 计算密集型任务用C语言编写,然后把.so链接库内容加载到Python中,因为执行C代码,GIL锁会释放,这样一来,就可以做到每个核都跑一个线程的目的!
计算密集型任务和I/O密集型任务
计算密集型任务
计算密集型任务的特点是要进行大量的计算,消耗CPU资源,比如计算圆周率、对视频进行高清解码等等,全靠CPU的运算能力。这种计算密集型任务虽然也可以用多任务完成,但是任务越多,花在任务切换的时间就越多,CPU执行任务的效率就越低,所以,要最高效地利用CPU,计算密集型任务同时进行的数量应当等于CPU的核心数。
计算密集型任务由于主要消耗CPU资源,因此,代码运行效率至关重要。Python这样的脚本语言运行效率很低,完全不适合计算密集型任务。对于计算密集型任务,最好用C语言编写。
I/O密集型任务
第二种任务的类型是IO密集型,涉及到网络、磁盘IO的任务都是IO密集型任务,这类任务的特点是CPU消耗很少,任务的大部分时间都在等待IO操作完成(因为IO的速度远远低于CPU和内存的速度)。对于IO密集型任务,任务越多,CPU效率越高,但也有一个限度。常见的大部分任务都是IO密集型任务,比如Web应用。
IO密集型任务执行期间,99%的时间都花在IO上,花在CPU上的时间很少,因此,用运行速度极快的C语言替换用Python这样运行速度极低的脚本语言,完全无法提升运行效率。对于IO密集型任务,最合适的语言就是开发效率最高(代码量最少)的语言,脚本语言是首选,C语言最差。
总结
综上,Python多线程相当于单核多线程,多线程有两个好处:CPU并行,IO并行,单核多线程相当于自断一臂。所以,在Python中,可以使用多线程,但不要指望能有效利用多核。如果一定要通过多线程利用多核,那只能通过C扩展来实现,不过这样就失去了Python简单易用的特点。不过,也不用过于担心,Python虽然不能利用多线程实现多核任务,但可以通过多进程实现多核任务。多个Python进程有各自独立的GIL锁,互不影响。