【Python】多线程和多进程开发以及GIL解释

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_41427568/article/details/90052590

前言

(碎碎念,嫌麻烦的可以跳过前言)
之前一直不太理解线程和进程,最近学了操作系统之后才慢慢有一个清楚的概念。这篇文章参考了很多大佬的博客,再加上一些自己的理解。平时写项目多用到Python,这篇文章就介绍如何使用Python实现多线程和多进程程序的开发,Here we go!!

进程与线程

进程

一个程序的执行实例就是一个进程,进程是系统进行资源分配和调度的一个独立单位,或者说进程本质上是资源的集合,它提供了执行程序所需的所有资源。
一个进程启动时都会最先产生一个线程,即为主线程,当用户在主线程中创建子线程时,可以出现一个进程中包含多个线程并发的情况。

线程

线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中实际运作的单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发执行多个线程,每条线程并行执行不同的任务。一个线程是一个执行上下文,即一个CPU执行时所需要的一串指令。
比如说一个程序的任务是读取文件并将文件内容进行输出,则我们就可以创建子线程进行I/O任务,主线程进行输出任务。这两个线程可以并发进行,也就是CPU给每一个线程一个极短的运行时间片,在时间片结束的时候切换线程,因为线程切换速度很快,CPU会给你一种在同一时间做了多个运算的幻觉。的确,在宏观上确实是多个线程同时运行,但是在微观上,是不同线程在极短时间内的交替运行。

进程与线程之间的区别

1、同一个进程中的线程共享同一内存空间,但是进程之间是独立的。
2、同一个进程中的所有线程的数据是共享的(进程通讯),进程之间的数据是独立的。
3、对主线程的修改可能会影响其他线程的行为,但是父进程的修改(除了删除以外)不会影响其他子进程。
4、线程是一个上下文的执行指令,而进程则是与运算相关的一簇资源。
5、同一个进程的线程之间可以直接通信,但是进程之间的交流需要借助中间代理来实现,比如说Queue(队列)或者Pipe(管道)。
6、创建新的线程很容易,但是创建新的进程需要对父进程做一次克隆。
7、一个线程可以操作同一进程的其他线程,但是进程只能操作其子进程。
8、线程启动速度快,进程启动速度慢(但是两者运行速度没有可比性)。
9、改变主线程(比如说优先权),可能会影响到其他线程。但是改变父进程不会影响子进程。

多线程

线程对象常用方法

start() 线程启动就绪,等待CPU调度。
join() 将子线程加入到主线程中,逐个执行每个线程,也就是主线程等待子线程执行完后,再继续向下执行。
run() 线程被CPU调度之后会自动执行线程对象的run()方法,如果向自定义线程类,直接重写线程类中run()方法即可。
setName() 为线程设置名称
getName() 获取线程名称
setDaemon(True) 把所有子线程变成主线程的守护线程

线程的创建方式

线程有两种方式,第一种是实例化线程对象,第二种是继承threading.Thread类来自定义线程类。
1、实例化线程对象

'''
created on May 10 10:55 2019

@author:lhy
'''
import threading
import time

def run_func(n):
	print("this is task{}".format(n))
	time.sleep(1)#延时1s
	print("task {} end".format(n))
	
t1=threading.Thread(target=run_func,args=(1,))
t2=threading.Thread(target=run_func,args=(2,))

t1.start()
t2.start()

结果

this is task1
this is task2
task 1 end
task 2 end

2、自定义线程类,继承threading.Thread,本质是重写Thread类中的run()方法。

'''
created on May 10 10:56 2019

@author:lhy
'''
import threading
import time

class MyThread(threading.Thread):
	def __init__(self,n):
		super(MyThread,self).__init__()#执行父类的构造函数
		self.n=n
	
	#overwrite
	def run(self):
		print("this is task{}".format(self.n))
		time.sleep(1)
		print("task{} end".format(self.n))
	
t1=MyThread(1)
t2=MyThread(2)

t1.start()
t2.start()

结果:

this is task1
this is task2
task1 end
task2 end

统计当前活跃的线程数

如果主线程执行的子线程结束比主线程慢,当主线程执行active_count()函数时,其他子线程都还没有执行完毕,因此主线程统计的线程数量num=sub_num(子线程的数量)+1(主线程本身)。

如果主线程执行的子线程结束比主线程快,当主线程执行active_count()函数时,其他子线程都执行完毕,因此主线程统计的线程数量num=1(主线程本身)。

可以通过threading.current_thread()输出当前正在执行的线程。

'''
created on May 10 10:10 2019

@author:lhy
'''
import threading 
import time

def run_func(n):
	print("running task{}".format(n))
	time.sleep(1)
	print("task{} end".format(n))

for i in range(3):
	t=threading.Thread(target=run_func,args=(i,))
	t.start()

#time.sleep(2)主线程停2s,等待子线程全部结束,这时候当前活跃的进程数应该只有1个,就是主线程
#输出当前函数所在线程
print(threading.current_thread())
#输出当前活跃的进程数
print(threading.active_count())

结果:

running task0
running task1
running task2
<_MainThread(MainThread, started 33188)>
4
task2 end
task1 end
task0 end

守护线程

使用setDaemon(True)把所有子线程变成主线程的守护线程,当主线程结束后,子线程也会随之结束。所以当主线程结束后,整个程序就退出了。
注意:不能让守护线程和非守护线程在一个进程中同时跑,会导致一些奇怪的bug

'''
created on May 10 11:30 2019

@author:lhy
'''
import threading
import time

def run(n):
	print("running task{}".format(n))
	time.sleep(0.5)
	print("sleep 0.5s")
	time.sleep(1)
	print("task{} end".format(n))

for i in range(3):
	t=threading.Thread(target=run,args=(i,))
	t.setDaemon(True)	#把子线程设置为守线进程,必须在start()之前设置
	t.start()

#开启一个非守护进程,但是这种方法会导致报错或者让守护进程的守护作用消失
#t=threading.Thread(target=run,args=(2333,))
#t.start()

#主线程停1s
time.sleep(1)
#输出活跃的线程数
print(threading.active_count())

print("主线程结束")

输出:

running task0
running task1
running task2
sleep 0.5s
sleep 0.5s
sleep 0.5s
4
主线程结束

可以看到子线程没有输出"task end"字符串。

万恶之源GIL

其实说GIL是万恶之源有点过分,但是的确是因为这个东西让Python的多线程被大家疯狂吐槽。
在非Python的环境中,单核的情况下,同时只有一个任务执行。多核时可以支持多个线程同时在多个核中执行,但是在Python环境中,无论有多少个核,同时只能有一个核并发的执行多线程。也就是说Python的多线程并不能调用CPU所有核同时运行线程,只能在一个核上并发的执行线程。而对于其他语言的多线程来讲,可以让多线程跑在不同的核上,实现真正意义上的多线程。所以对于4核的CPU,如果使用Python多线程,最高的CPU占比只有25%左右。

GIL实验

GIL实验:
我们在一个进程中建立10000个子线程,并在子线程中用while循环做1000000次加法,按照多线程设计的理念,CPU应该在短时间内就会达到100%的利用率,但是实际上的利用率只在25%左右徘徊。

import threading
import time

def run():
	n=0
	while n<1000000:
		n+=1
	print("子线程结束")
	
for i in range(10000):
	t=threading.Thread(target=run,args=())
	t.start()

结果:
在这里插入图片描述
其实上述问题就是GIL导致的,GIL的全称是Global Interpreter Lock(全局解释器锁),来源是python设计之初的考虑,为了数据安全所做的决定。每个进程只有一个GIL,某个线程想要执行,必须先拿到GIL,我们可以把GIL看作是“通行证”。所以一个进程在一个CPU核上执行,那么这个进程中的子线程也只能在这一个核上执行,并不能做到多核充分利用。

多线程的工作过程

Python多线程的工作过程:
1、拿到公共数据
2、申请GIL
3、Python解释器调用os原生线程
4、os操作CPU执行运算
5、当该线程执行时间片到后,无论运算是否已经执行完,GIL都被要求释放,并将没有执行完的线程放入就绪队列,等待下一次获取GIL锁执行。
6、进而由其他线程重复上面的过程
7、等其他线程执行完后,又会切换到之前的线程(从他记录的上下文继续执行),整个过程是每个线程执行自己的运算,当执行时间到就进行切换(context switch)。

Python执行效率

Python针对不同类型的代码,执行效率也是不同的。
1、CPU密集型代码(各种循环处理、计算等等),在这种情况下,由于计算工作多,使用多核进行并行计算效率会大大提高,而且对于密集型任务代码,线程时间片结束后触发GIL的释放与再竞争(多个线程来回切换当然是需要消耗资源的),所以python下的多线程对CPU密集型代码并不友好,应该使用多进程处理。

2、IO密集型代码(文件处理、网络爬虫等涉及文件读写的操作),多线程能够有效提升效率(单线程下有IO操作会进行IO等待,造成不必要的时间浪费,而开启多线程能在线程A等待时,自动切换到线程B,可以不浪费CPU的资源,从而能提升程序执行效率)。所以python的多线程对IO密集型代码比较友好。

如果想在Python中充分使用多核CPU的强大功能,就不能使用多线程,应该使用多进程。因为每个进程都有各自独立的GIL,互不干扰,这样就可以做到真正意义上的并行处理(在多个处理器上执行任务)。在Python中对于多核CPU,多进程的执行效率优于多线程。

线程锁

每个线程在执行start()函数之后,并不是立即执行的,而是等待CPU的调度。CPU开始通过线程顺序进行调度,但是线程之间切换的调度存在一定的随机性,是不可控的。当多个线程修改同一个数据时,就可能出现冲突的情况,也就是脏数据。在Python3中已经存在许多保护措施,尽量让线程之间的调度更加安全,但是还是存在一些问题,比如说执行不同线程执行时间不同导致的结果异常:

'''
created on May 10 15:38 2019

@author:lhy
'''
import threading
import time

num=1


def run(flag):
	global num
	
	if flag==1:
		num+=1
	elif flag==2:
		num-=1
	elif flag==3:
		#当准备执行乘法的时候,线程由于某种原因停滞了1s
		time.sleep(1)
		num*=3
	elif flag==4:
		num/=2
	
		
if __name__=="__main__":
	t1=threading.Thread(target=run,args=(1,))#加一 num=2
	t2=threading.Thread(target=run,args=(3,))#乘三 num=6
	t3=threading.Thread(target=run,args=(2,))#减一 num=5
	t4=threading.Thread(target=run,args=(3,))#乘三 num=15
	t5=threading.Thread(target=run,args=(1,))#加一 num=16
	t6=threading.Thread(target=run,args=(4,))#除二 num=8
	
	t1.start()
	t2.start()
	t3.start()
	t4.start()
	t5.start()
	t6.start()
	
	t1.join()
	t2.join()
	t3.join()
	t4.join()
	t5.join()
	t6.join()
	
	print(num)

num是全局变量,所以任何线程都可以访问并改变它的值。
执行以上代码,应该得出正确的num结果是8,但是由于在乘法部分由于某种原因线程停滞了1s(代指的是线程调度中的一些问题,我们这里拿1s的延迟举例),最终得出的结果是9。其实问题就出在停滞这一秒中,当程序执行到乘法的时候,这个线程会停滞1s,在这一秒之中,其他没有停滞的线程并没有等待乘法的执行,而是直接对num进行了操作。加减乘除的执行顺序就这样被打乱,最终出现了结果的异常。

互斥锁(mutex)

那么,有办法可以让多线程处理某一共享资源的时候可以独占这个资源,不让其他线程访问使用,也就是给这个共享资源上一把锁,等到这个线程处理结束的时候,打开这把锁,释放资源,供其他线程使用。于是,锁的概念就被提出来了。
为了防止以上情况发生,就出现了互斥锁(Lock)

'''
created on May 10 16:08 2019

@author:lhy
'''
import threading
import time

num=1
lock=threading.Lock()#实例化一个锁对象

def run(flag):
	global num
	lock.acquire()#获得锁
	if flag==1:
		num+=1
	elif flag==2:
		num-=1
	elif flag==3:
		time.sleep(1)
		num*=3
	elif flag==4:
		num/=2
	lock.release()#释放锁
		
if __name__=="__main__":
	t1=threading.Thread(target=run,args=(1,))
	t2=threading.Thread(target=run,args=(3,))
	t3=threading.Thread(target=run,args=(2,))
	t4=threading.Thread(target=run,args=(3,))
	t5=threading.Thread(target=run,args=(1,))
	t6=threading.Thread(target=run,args=(4,))
	
	t1.start()
	t2.start()
	t3.start()
	t4.start()
	t5.start()
	t6.start()
	
	t1.join()
	t2.join()
	t3.join()
	t4.join()
	t5.join()
	t6.join()
	
	print(num)

运行后,发现结果正常,执行乘法线程的时候将共享资源num锁起来,其他线程不能访问到,只能等待乘法线程释放资源才能继续执行,这样,加减乘除的顺序就被保证不变了。

递归锁

RLock类的用法和Lock类一模一样,但它支持嵌套锁,在多个锁没有释放的时候一般会使用RLock类。

'''
created on May 10 16:18 2019

@author:lhy
'''
import threading
import time

global_num=0

rlock=threading.RLock()

def run_func():
	rlock.acquire()#获取锁
	global global_num
	global_num+=1
	time.sleep(1)
	print(global_num)
	rlock.release()#释放锁
	
for i in range(10):
	t=threading.Thread(target=run_func)
	t.start()
	

信号量机制

互斥锁同时只允许一个线程访问修改共享资源,而信号量机制则是同时允许一定数量的线程更改数据,比如说一次允许三个线程同时访问并修改共享资源。

'''
created on May 10 16:24 2019

@author:lhy
'''
import threading
import time


def run(n):
    semaphore.acquire()   #加锁
    time.sleep(1)
    print("run the thread:%s\n" % n)
    semaphore.release()     #释放


num = 0
semaphore = threading.BoundedSemaphore(4)  # 最多允许4个线程同时运行

for i in range(20):
    t = threading.Thread(target=run, args=("t-%s" % i,))
    t.start()

if threading.active_count() == 1:
    print('-----all threads done-----')

定时线程

Timer类是定时线程,指定n秒后执行某线程。

from threading import Timer
import time

def run_func():
	print("I'm here")
	
t=Timer(1,run_func)
t.start()#经过1s,"I'm here"会被打印出来
time.sleep(0.9)
print("主线程经过了0.9s")

输出:

主线程经过了0.9s
I'm here

多进程

在UNIX中,每个进程都是由父进程提供的。每次启动一个子进程就从父进程克隆一份数据,但是进程之间的数据本身不可以共享。
创建子线程示例:

'''
created on May 10 16:45 2019

@author:lhy
'''
from multiprocessing import Process
import time

def f(name):
	time.sleep(2)
	print('this process name is '+name)

if __name__=="__main__":
	p=Process(target=f,args=('Alan',))#创建子进程
	p.start()
	p.join()

下面输出父进程与子进程的pid,便于找到多进程之间的关系:

'''
created on May 10 17:30 2019

@author:lhy
'''
from multiprocessing import Process
import os

def print_info(title):
	print(title)
	print('module name: {}'.format(__name__))
	print('parent process: {}'.format(os.getppid()))#获取父进程id
	print('process id: {}'.format(os.getpid()))#获取自己的进程id

def f(name):
	print_info('这是在子进程中调用的函数')
	print("hello {}".format(name))
	
if __name__=='__main__':
	print_info('这是父进程中调用的函数')
	p=Process(target=f,args=('Alan',))
	p.start()
	p.join()

以下是代码运行的结果,可以看到__main__也是有父进程的,且__main__创建的子进程的父进程的pid就是__main__的pid。

这是父进程中调用的函数
module name: __main__
parent process: 42204
process id: 19600
这是在子进程中调用的函数
module name: __mp_main__
parent process: 19600
process id: 20764
hello Alan

进程间通信

前面介绍过,由于进程之间的数据是不共享的,所以不会出现多线程通过GIL来控制线程的切换而出现的问题。多进程之间的通信可以通过Queue(队列)或者Pipe(管道)来实现,也可以通过后面介绍的Manager实现。

Queue()

创建一个Queue之后,所有进程都可以通过这个Queue存入或拿出信息,遵循“先入先出FIFO”原则。

multiprocessing的Queue常用的函数及其功能:
q=Queue([maxsize]) 初始化队列,可以通过maxsize指定队列最大的容量是多少,如果maxsize的数值小于等于0,这个队列则为无穷大
Queue.qsize() 返回队列中存在元素的数量
Queue.empty() 如果队列为空,返回True,反之False
Queue.full() 如果队列满了,返回True,反之False
Queue.get([block[, timeout]])获取队列中最先入队的元素,并将这个元素从队列中弹出,timeout是是队列堵塞时等待的最长时间
Queue.get_nowait() 相当Queue.get(False)
Queue.put(item) 将item放入队列,timeout是队列堵塞时等待的最长时间
Queue.put_nowait(item) 相当Queue.put(item, False)

使用multiprocessing中Queue的例子:

'''
created on May 10 19:16 2019

@author:lhy
'''
from multiprocessing import Process, Queue
 
def f(q,i):
	q.put([i, None, 'hello'])#向队列中写入
	print("23333")
 
if __name__ == '__main__':
	q = Queue()#创建一个队列存储进程消息
	for i in range(10):
		p = Process(target=f, args=(q,i,))
		p.start()
		p.join()
	print(q.qsize())
	while not q.empty():
		print(q.get())

我们知道,在Python语法中也存在一个queue,使用from queue import Queue来使用,他们二者的区别如下:
1.from queue import Queue
这个是普通的队列模式,类似于普通列表,先进先出模式,get方法存在阻塞请求,直到有数据get出来为止。Queue.Queue是进程内非阻塞队列,一般用于多线程通信,因为CPython中GIL的存在,Python的多线程只能在一个进程中运行,所以多线程的线程还是在一个进程中运行,不存在不同进程之间的通信。

2.from multiprocessing.Queue import Queue(各子进程共有)
这个是多进程并发的Queue队列,用于解决多进程间的通信问题。普通Queue实现不了。例如来跑多进程对一批IP列表进行运算,运算后的结果都存到Queue队列里面,这个就必须使用multiprocessing提供的Queue来实现。

在multiprocessing模块的官方文档中有这样一句话:
Queue implements all the methods of queue.Queue except for task_done() and join()
是的,Python中普通的Queue比multiprocessing中的Queue多两个常用函数,分别是Queue.task_done()Queue.join()。必须要注意的是,这两个函数在multiprocessing的Queue中并不能使用。Python官方文档对这两个函数是这样解释的:

Queue.join() 堵塞操作,实际上意味着,运行到这句的程序需要等到队列为空,再执行接下来的操作。
Queue.task_done() 在完成一项工作之后,Queue.task_done()函数向任务已经完成的队列发送一个信号,一般使用在get之后调用,告诉队列一个任务处理完成。当然也可以不在调用get之后使用,直接使用也是可以的,不过不建议这样使用,这样会让任务实际剩余数量和join()函数得到的数量不一致。

说了这么多但是还是一头雾水?
大白话来讲就是往队列中put几次,就要调用task_done几次,join()函数从put和task_done的次数判断queue中的任务是否已经执行完。单用put和get是不能告诉join()函数当前任务执行的情况,所以要在get之后,加上一句task_done()函数告诉join()函数一个任务是否已经完成了。
我们使用多线程和Python自带的queue做一个关于task_done()与join()的例子:

import threading
from queue import Queue
from time import sleep

q=Queue() #初始化队列

def func(q):
	sleep(0.5)
	item=q.get() #将任务从队列中弹出来,获取任务
	print('{} get {},{} left'.format(threading.currentThread().ident,item,q.qsize()))
	q.task_done() #告诉join函数一个任务处理完成了

if __name__=='__main__':
	for i in range(5): #创建5个线程求获取queue中的任务
		t=threading.Thread(target=func,args=(q,))
		t.start()
	for i in range(10): #入队10个任务
		q.put([i])

	#q.task_done() #put()之后直接跟task_done()会报错

	for i in range(5): #将剩下5个任务直接task_done出来
		print('this is task {}'.format(i))
		q.task_done()

	print('main treading running')
	q.join() #堵塞,直到调用10此task_done
	print('main threading end')

注意:使用task_done()函数的时候需要注意一点:尽量不要出现put()之后直接使用task_done()的情况,否则会报错task_done() called too many times的错误。

Pipe()

Pipe的本质是进程之间的数据传递,而不是数据共享,这和socket有点像。pipe()返回两个连接对象分别表示管道的两端,每端都有send()和recv()方法。如果两个进程试图在同一时间的同一端进行读取和写入那么,这可能会损坏管道中的数据。

'''
created on May 10 19:25 2019

@author:lhy
'''
from multiprocessing import Process,Pipe

def f(conn):
	conn.send([1,None,'hello'])#子进程通过管道发送消息1
	conn.send([2,None,'hello'])#子进程通过管道发送消息2
	conn.close()#关闭管道

if __name__=='__main__':
	parent_conn,child_conn=Pipe()
	p=Process(target=f,args=(child_conn,))#建立子线程
	p.start()
	print(parent_conn.recv())#输出接受到的信息1
	print(parent_conn.recv())#输出接受到的信息2
	p.join()

输出:

[1, None, 'hello']
[2, None, 'hello']

Manager

通过Manager可实现进程间数据的共享。Manager()返回的manager对象会通过一个服务进程,来使其他进程通过代理的方式操作Python对象。manager对象支持 list, dict, Namespace, Lock, RLock, Semaphore, BoundedSemaphore, Condition, Event,Barrier, Queue, Value ,Array.
下面举例使用list和dict:

'''
created on May 10 19:44 2019

@author:lhy
'''
from multiprocessing import Process,Manager

def f(dict,list):
	dict[1]='1'
	dict['25']=25
	dict[1.25]=None
	list.append(1)
	print(list)
	
if __name__=='__main__':
	with Manager() as manager:
		dict=manager.dict()
		
		list=manager.list()
		p_list=[]
		for i in range(10):
			p=Process(target=f,args=(dict,list))
			p.start()
			p_list.append(p)
		for res in p_list:
			res.join()
		
		print(dict)
		print(list)

上述代码输出:

[1]
[1, 1]
[1, 1, 1]
[1, 1, 1, 1]
[1, 1, 1, 1, 1]
[1, 1, 1, 1, 1, 1]
[1, 1, 1, 1, 1, 1, 1]
[1, 1, 1, 1, 1, 1, 1, 1]
[1, 1, 1, 1, 1, 1, 1, 1, 1]
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
{1: '1', '25': 25, 1.25: None}
[1, 1, 1, 1, 1, 1, 1, 1, 1, 1]

进程锁

在多线程中存在互斥锁、递归锁、信号量机制,同样在多进程中也存在锁。下面就简单用进程锁防止多个进程的输出内容在同一块屏幕显示乱序的情况。

'''
created on May 10 19:53 2019

@author:lhy
'''
from multiprocessing import Process,Lock

def fun(lock,i):
	lock.acquire()
	try:
		print('hello task{}'.format(i))
	finally:
		lock.release()

if __name__=='__main__':
	lock=Lock()#实例化一个锁对象
	
	for i in range(10):
		Process(target=fun,args=(lock,i)).start()

进程池

由于进程启动的开销比较大,使用多进程的时候会导致大量内存空间被消耗。为了防止这种情况发生可以使用进程池,(由于启动线程的开销比较小,所以不需要线程池这种概念,多线程只会频繁得切换cpu导致系统变慢,并不会占用过多的内存空间)
进程池中常用的方法:
apply() 同步执行(串行)。
apply_async() 异步执行(并行)。
terminate() 立刻关闭进程池。
join() 主进程等待所有子进程执行完毕。必须在close或terminate()之后。
close() 函数会等待所有进程结束后,才关闭进程池。

'''
created on May 10 20:00 2019

@author:lhy
'''
from multiprocessing import Process,Pool
import time

def Fun1(i):
	time.sleep(2)
	return i+100

def Fun2(name):
	print('-->exe done {}'.format(name))

if __name__=='__main__':
	pool=Pool(5)	#允许进程吃最多同时放入五个进程
	for i in range(10):
		#Fun1子进程执行完后,才会执行callback函数,否则不会执行,而且callback由父进程执行
		#异步执行进程池中的进程
		pool.apply_async(func=Fun1,args=(i,),callback=Fun2)
	
	print(23333)
	pool.close()#关闭进程池
	pool.join()#加入主线程
	print('end')

输出:

23333
-->exe done 100
-->exe done 101
-->exe done 102
-->exe done 104
-->exe done 103
-->exe done 105
-->exe done 106
-->exe done 107
-->exe done 108
-->exe done 109
end

在输出的时候,100-104会先输出,然后等待2s,105-109再被输出。等于说真正进入CPU并行执行的进程一组只有5个,进程apply之后,由CPU进行进程调度执行。

进程池内部维护一个进程序列,当使用时,去进程池中获取一个进程,如果进程池序列中没有可供使用的进程,那么程序就会等待,直到进程池中有可用进程为止。在上面的程序中产生了10个进程,但是只能有5同时被放入进程池,剩下的都被暂时挂起,并不占用内存空间,因为挂起操作会将程序挂到外存中。等前面的五个进程执行完后,再将剩下5个被挂起的程序拿入进程池执行。

使用多进程让自己电脑CPU拉满性能?
对上文GIL的测试程序更改,改成多进程,看看原来25%的CPU使用率能拉满不能:

from multiprocessing import Process

def run():
	n=0
	while n<1000000:
		n+=1
	print("子进程结束")

if __name__=='__main__':
	for i in range(100000):
		p=Process(target=run)
		p.start()

运行以上代码,CPU性能被瞬间拉满:
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/qq_41427568/article/details/90052590