GIL锁VS普通互斥锁

一、GIL锁

刚开始的时候只有单核cpu
单核 --线程是cpu调度的最小单位,在cpython就是python解释器进程,在这个进程里可以开多条线程,但同一时刻只能被一个cpu调度一条线程去执行。
python中垃圾回收是垃圾回收线程,是单独起一条线程来执行的,在垃圾回收线程在执行时别的线程不会执行,也就不会出现并发安全的问题。
之后发展成有多核cpu 2核
----多线程被多cpu调度,就会出现并发安全问题
(比如有2个cpu,这个线程在这个cpu执行,另外一个垃圾回收线程在另一个cpu执行的,当垃圾回收线程在执行的时候可能跟另一个在执行中的cpu操作同一变量,就会导致并发安全问题),
龟叔就想到弄一把大锁,Cpython解释器内置了GIL锁解决了此问题。在cpython解释器上执行的线程,只有获取到GIL互斥锁的线程才能被调度去执行,也就变成了同一时刻一个cpu的一个线程执行
(所以python无法利用多核优势),这样就解决了并发安全的问题,但又出现了python不能利用多核优势这个问题,
即便是有10个cpu10条线程,同一时刻也只有一个cpu的一条线程可以调度去执行
之后为了提高效率,有了多进程,多进程里可以有多个线程,比如开3个进程,那么每个进程有一个线程可以被cpu调度去执行,进程之间数据是隔离,不存在数据并发安全问题,
所以要执行cpu(计算)密集型要开多进程,要执行io密集型要开多线程 之后进程消耗资源太大
----就有了协程 协程就是单线程下的并发,自己处理io操作,一遇到io操作自己切,切过来执行完了之后再回去再去执行另外一个

二、为什么有了GIL锁还需要互斥锁?

GIL已经保证了只有一个线程在运行了 那么其他线程应该是没有权限再操作资源了呀?

GIL锁叫全局解释器锁,因为python是解释型语言,要做垃圾回收,比如cpython解释器里同一进程开3个线程,进程内数据是共享的,当垃圾回收线程要工作时其他线程就会停止,
此时可以看到哪个线程的引用计数为0了,再决定是否要做回收。 现在有3条线程,分别是a
=0, a=a+1,a=a+1 正常情况下就会加到3,如果多个线程并发起来执行,数据就会乱了(因为线程1拿到1还没来得及做累加,遇到io或者时间片到了,cpu会让出去执行
权限,需要切线程;接着调度了2执行,此时2拿到的a还是0,此时又还没来得及做累加,遇到io或者时间片到了,cpu会让出去执行权限,需要切线程;接着调度了3执行,此时3拿到的a还是0,
执行a+=1之后a=1了,写回去了1,之后cpu调度到1线程a原来等于0,a+1变成了1写回去了,之后cpu调度到2线程a原来等于0,a+1变成了1写回去了,就这样得到的结果与我们想象中的不一样了。


GIL锁是无法控制用户级别的安全的,所以就需要加个互斥锁,互斥锁是只要1拿到之后变量没改完就不释放,那就一直等着改完之后才给2给3,就保障了数据安全。 GIL锁本质也是互斥锁,但控制的级别不一样。GIL锁只能保障多进程的数据安全就是全cpython解释器的数据安全,普通互斥锁用来保障同一进程下的多线程的数据安全,使用级别不同。
-------------------------------------------------------------------------------------------------------------------------------------------------
这个只是cpython解释器会遇到的问题,编译型语言不会出现这种问题,因为编译型语言需要先编译,编译完了能够监控到谁用谁不用,所以不会存在gil锁的问题。
其他语言任何情况尽量开线程尽量不用开进程。

 三、拓展:GIL锁

cpu和GIL必须都具备才可以执行代码

拿到cpu权限-》拿到GIL解释器锁-》执行代码

​在python3.2之后GIL有了新的实现,目的是为了解决that GIL thrashing问题,这是Antoine Pitrou的功劳

GIL解释器锁会在两种情况下释放

1.主动释放:自己主动交出来

遇到IO操作或者分配的cpu时间片到时间了

注意,GIL存在的意义在于维护线程安全,x=10涉及到io操作,如果也被当成普通的io操作,主动交出GIL,那么一定会出现数据不安全问题,所以x=10一定是被区分对待了

至于x=10如何实现的被区分对待,这其实很好理解,任何的io操作都是向操作系统发送系统调用,即调用操作系统的某一接口实现的,比如变量赋值操作肯定是调用了一种接口,文件读写操作肯定也是调用了一种接口,网络io也是调用了某一种接口,这就给区分对待提供了实现的依据,即变量赋值操作并不属于主动释放的范畴,这样GIL在线程安全方面才会有所作为

2.被动释放

python3.2之后定义了一个全局变量

/* Python/ceval.c */
...
static volatile int gil_drop_request = 0;

 

注意当只有一个线程时,该线程会一直运行,不会释放GIL,当有多个线程时

例如thead1,thread2

如果thread1一直没有主动释放掉GIL,那肯定不会让他一直运行下去啊

实际上在thread1运行的过程时,thread2就会执行一个cv_wait(gil,TIMEOUT)的函数

(默认TIMEOUT值为5milliseconds,但是可以修改),一旦到了时间,就会将全局变量

gil_drop_request = 1;,线程thread1就会被强制释放GIL,然后线程thread2开始运行并

返回一个ack给线程thread1,线程thread1开始调用cv_wait(gil,TIMEOUT)

3.详见图解:http://www.dabeaz.com/python/UnderstandingGIL.pdf

 三、协程

解决单线程下的并发,遇到IO只会在自己的线程下切换,不会切到别的线程上去

 

posted @ 2020-11-13 16:38  1024bits  阅读(197)  评论(0编辑  收藏  举报