本节重点
- 掌握Cpython的GIL解释器锁的工作机制
- 掌握GIL与互斥锁
- 掌握Cpython多线程和多过程各自的应用场景
一 引子
定义: In CPython, the global interpreter lock, or GIL, is a mutex that prevents multiple native threads from executing Python bytecodes at once. This lock is necessary mainly because CPython’s memory management is not thread-safe. (However, since the GIL exists, other features have grown to depend on the guarantees that it enforces.) 结论:在Cpython在解释器中,同一过程下打开的多线程只能同时执行一个线程,多核优势无法利用
首先要明确的是,首先要明确的是GIL并不是Python它正在实现它的特性Python解析器(CPython)时间引入的概念。就好比C 是一套语言(语法)标准,但可以用不同的编译器编译成可执行代码。>例如,著名的编译器GCC,INTEL C ,Visual C 等。Python同样的代码也可以通过CPython,PyPy,Psyco等不同的Python执行环境。像其中的JPython就没有GIL。然而因为CPython在大多数环境下默认Python执行环境。所以在很多人的概念中CPython就是Python,也就想当然的把GIL归结为Python语言缺陷。所以这里要明确一点:GIL并不是Python的特性,Python完全可以不依赖GIL
二 GIL介绍
GIL本质是相互排斥锁。由于它是相互排斥锁,所有相互排斥锁的本质都是相同的。它们将并发操作转换为串行,以控制共享数据只能在同一时间被任务修改,以确保数据安全。
可以肯定的是,为了保护不同数据的安全,应添加不同的锁。
要想了解GIL,首先确定一点:每次执行:python程序,会产生一个独立的过程。python test.py,python aaa.py,python bbb.py会产生三种不同python进程
#test.py内容 import os,time print(os.getpid()) time.sleep(1000) #打开终端执行 python3 test.py #在windows下查看 tasklist |findstr python #在linux下下查看 ps aux |grep python
在一个python不仅有test.py主线程或主线程打开的其他线程,以及解释器打开的垃圾回收等解释器级别的线程。简而言之,毫无疑问,所有的线程都在这个过程中运行
1.所有数据是共享的,其中代码作为一种数据也被所有线程共享(test.py所有代码和Cpython解释器的所有代码) 例如:test.py定义函数work(代码内容如下图所示)所有线程都可以在过程中访问work所以我们可以打开三个线程target都指代码,访问意味着可以执行。 2.所有线程的任务都需要将任务代码作为参数传输给解释器的代码来执行,即如果所有线程都想运行自己的任务,首先需要解决的是访问解释器的代码。 2.所有线程的任务都需要将任务代码作为参数传输给解释器的代码来执行,即如果所有线程都想运行自己的任务,首先需要解决的是访问解释器的代码。
综上:
若多线程target=work,所以执行过程是
多个线程首先访问解释器的代码,即获得执行权限,然后将target给解释器的代码执行
三 GIL与Lock
聪明的学生可能会问这个问题:Python已经有一个GIL为了确保只有一个线程可以同时执行,为什么它仍然需要在这里lock?
首先,我们需要达成共识:锁的目的是保护共享数据,同时只有一个线程来修改共享数据
然后,我们可以得出结论,为了保护不同的数据,应该添加不同的锁。
最后,问题很清楚,GIL 与Lock这是两把锁。保护数据不同。前者是解释器级数据(当然是解释器级数据,如垃圾回收数据),后者是保护用户开发的应用程序数据。GIL不负责此事,用户只能定制加锁处理,即Lock,如下图
分析:
1、100个线程GIL锁,即抢执行权限 2.必须先有一个线程GIL(暂称线程1)然后开始执行,一旦执行就会得到lock.acquire() 3.线程1很有可能在运行完成之前就被另一个线程2抓住了GIL,然后开始运行,但线程2发现互斥锁lock它还没有被线程1释放,所以它被阻塞了,被迫交出执行权,即释放GIL 4.直到线程1重新获取GIL,从上次暂停的位置开始,直到互斥锁正常释放lock,然后重复其他线程2 3 4的过程
代码示范
from threading import Thread,Lock import os,time def work(): global n lock.acquire() temp=n time.sleep(0.1) n=temp-1 lock.release() if __name__ == '__main__': lock=Lock() n=100 l=[] for i in range(100): p=Thread(target=work) l.append(p) p.start() for p in l: p.join() print(n) #结果必须是0,从原来的并发执行到串行,牺牲了执行效率,确保了数据安全,结果可能是99
四 GIL与多线程
有了GIL在同一时刻和同一过程中,只执行了一个线程
听到这里,一些学生立即问:过程可以使用多核,但成本很高,而且python的多线程开销小,但却无法利用多核优势,也就是说python没用了,php是最牛逼的语言吗?
别担心,我妈还没说完。
1、cpu是用来计算还是用来计算?I/O的? 2、多cpu,这意味着多个核可以并行完成计算,因此多核可以提高计算性能 3、每个cpu一旦遇到I/O堵塞,还需要等待,多检查I/O操作无用
工人相当于cpu,计算相当于工人在工作,I/O堵塞相当于为工人提供所需原材料的过程。如果工人在工作过程中没有原材料,工人在等待原材料到来之前需要停止工作。
如果你的工厂做的大部分任务都有准备原材料的过程(I/O密集型),那么你有多少工人,意义不大,不如一个人,让工人在等材料的过程中做其他工作,
另一方面,如果你的工厂有完整的原材料,工人越多,效率就越高
1.对于计算,cpu越多越好,但对I/O再多cpu也没用 2.当然,对于一个程序的运行cpu执行效率的提高肯定会提高(无论提高多少,总会提高),因为一个程序
基本上不是纯计算或纯计算的I/O,因此,我们只能相对地看一个程序是计算密集型还是密集型I/O密集型,进一步分析python多线程有用吗?
方案一:打开四个过程 方案二:在一个过程中打开四个线程
若四个任务是计算密集型,没有多核并行计算,方案一徒增加了创建过程的费用,方案二胜 若四个任务是I/O密集型,方案一创建过程成本大,过程切换速度远低于线程,方案二胜
若四个任务是计算密集型,多核意味着并行计算python在一个过程中,同一时刻只有一个线程执行不需要多核,方案赢了 若四个任务是/O密集型,再多的核也解决不了I/O问题,方案二胜
现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。
五 多线程性能测试
from multiprocessing import Process
from threading import Thread
import os,time
def work():
res=0
for i in range(100000000):
res*=i
if __name__ == '__main__':
l=[]
print(os.cpu_count()) #本机为4核
start=time.time()
for i in range(4):
p=Process(target=work) #耗时5s多
p=Thread(target=work) #耗时18s多
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start))
from multiprocessing import Process
from threading import Thread
import threading
import os,time
def work():
time.sleep(2)
print('===>')
if __name__ == '__main__':
l=[]
print(os.cpu_count()) #本机为4核
start=time.time()
for i in range(400):
# p=Process(target=work) #耗时12s多,大部分时间耗费在创建进程上
p=Thread(target=work) #耗时2s多
l.append(p)
p.start()
for p in l:
p.join()
stop=time.time()
print('run time is %s' %(stop-start))
多线程用于IO密集型,如socket,爬虫,web
多进程用于计算密集型,如金融分析