Python GIL(Global Interpreter Lock)

Posted tu240302975

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Python GIL(Global Interpreter Lock)相关的知识,希望对你有一定的参考价值。

一,介绍

定义:
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会产生3个不同的python进程

验证python test.py 只会产生一个进程

‘‘‘
#验证python test.py只会产生一个进程
#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 所有线程的任务,都需要将任务的代码当做参数传给解释器的代码去执行,即所有的线程要想运行自己的任务,首先需要解决的是能够访问到解释器的代码。

综上:

  如果多个线程的target=work,那么执行流程是

  多个线程先访问到解释器的代码,即拿到执行权限,然后将target的代码交给解释器的代码去执行

  解释器的代码是所有线程共享的,所以垃圾回收线程也可能访问到解释器的代码而去执行,这就导致了一个问题:对于同一个数据100,可能线程1执行x=100的同时,而垃圾回收执行的是回收100的操作,解决这种问题没有什么高明的方法,就是加锁处理,如下图的GIL,保证python解释器同一时间只能执行一个任务的代码

技术图片

 

三,GIL与Lock

机智的同学可能会问到这个问题:Python已经有一个GIL来保证同一时间只能有一个线程来执行了,为什么这里还需要lock?

首先,我们需要达成共识:锁的目的是为了保护共享的数据,同一时间只能有一个线程来修改共享的数据

然后,我们可以得出结论:保护不同的数据就应该加不同的锁。

最后,问题就很明朗了,GIL 与Lock是两把锁,保护的数据不一样,前者是解释器级别的(当然保护的就是解释器级别的数据,比如垃圾回收的数据),后者是保护用户自己开发的应用程序的数据,很明显GIL不负责这件事,只能用户自定义加锁处理,即Lock

GIL保护的是解释器级别的数据,保护用户自己的数据则需要自己加锁处理,如下图:

技术图片

分析:

1、100个线程去抢GIL锁,即抢执行权限
 
2、肯定有一个线程先抢到GIL(暂且称为线程1),然后开始执行,一旦执行就会拿到lock.acquire()
 
3、极有可能线程1还未运行完毕,就有另外一个线程2抢到GIL,然后开始运行,但线程2发现互斥锁lock还未被线程1释放,于是阻塞,被迫交出执行权限,即释放GIL
 
4、直到线程1重新抢到GIL,开始从上次暂停的位置继续执行,直到正常释放互斥锁lock,然后其他的线程再重复2 3 4的过程

代码示例:

# _*_ coding: utf-8 _*_
from threading import Thread
from threading import Lock
import time
 
n =100
def task():
    global n
    mutex.acquire()
    temp = n
    time.sleep(0.1)
    n = temp - 1
    mutex.release()
 
if __name__ == ‘__main__‘:
    mutex = Lock()
    t_l = []
    for i in range(100):
        t = Thread(target=task)
        t_l.append(t)
        t.start()
 
    for t in t_l:
        t.join()
    print("主",n)

  结果:肯定为0,由原来的并发执行变为串行,牺牲了执行效率保证了数据安全,不加锁则结果可能为99

主 0

 

四,GIL与多线程

  有了GIL的存在,同一时刻同一进程中只有一个线程被执行

  听到这里,有的同学立马质问:进程可以利用多核,但是开销大,而python的多线程开销小,但却无法利用多核优势,也就是说python没用了?

所以说要解决这个问题,我们需要在几个点上达成一致:

1. cpu到底是用来做计算的,还是用来做I/O的?
 
2. 多cpu,意味着可以有多个核并行完成计算,所以多核提升的是计算性能
 
3. 每个cpu一旦遇到I/O阻塞,仍然需要等待,所以多核对I/O操作没什么用处

  一个工人相当于cpu,此时计算相当于工人在干活,I/O阻塞相当于为工人干活提供所需原材料的过程,工人干活的过程中如果没有原材料了,则工人干活的过程需要停止,直到等待原材料的到来。

  如果你的工厂干的大多数任务都要有准备原材料的过程(I/O密集型),那么你有再多的工人,意义也不大,还不如一个人,在等材料的过程中让工人去干别的活,

  反过来讲,如果你的工厂原材料都齐全,那当然是工人越多,效率越高

结论:

对计算来说,cpu越多越好,但是对于I/O来说,再多的cpu也没用
 
当然对运行一个程序来说,随着cpu的增多执行效率肯定会有所提高(不管提高幅度多大,总会有所提高),这是因为一个程序基本上不会是纯计算或者纯I/O,
所以我们只能相对的去看一个程序到底是计算密集型还是I/O密集型,从而进一步分析python的多线程到底有无用武之地

假设我们有四个任务需要处理,处理方式肯定是需要玩出并发的效果,解决方案可以是:

方案一:开启四个进程
 
方案二:一个进程下,开启四个线程

单核情况下,分析结果:

如果四个任务是计算密集型,没有多核来并行计算,方案一徒增了创建进程的开销。方案二胜
 
如果四个任务是I/O密集型,方案一创建进程的开销大,且金成德切换速度远不如线程,方案二胜

多核情况下,分析结果:

如果四个任务是计算密集型,多核意味着并行计算,在python中一个进程中同一时刻只有一个线程执行,并不上多核。方案一胜
 
如果四个任务是I/O密集型,再多的核也解决不了I/O问题,方案二胜

结论:

现在的计算机基本上都是多核,python对于计算密集型的任务开多线程的效率并不能带来多大性能上的提升,甚至不如串行(没有大量切换),但是,对于IO密集型的任务效率还是有显著提升的。

 

五,多线程性能测试

如果并发的多个任务是计算密集型:多进程效率高

# _*_ coding: utf-8 _*_
#计算密集型用多进程
from multiprocessing import Process
from threading import Thread
import os
import time
 
def work():
    res = 0
    for i in range(100000000):
        res *= 1
 
if  __name__ == ‘__main__‘:
    l = []
    print(os.cpu_count())
    start = time.time()
    for i in range(8):
        # p = Process(target=work)
        #run time is :43.401108741760254
        t = Thread(target=work)
        #run time is : 62.395447731018066
        # l.append(p)
        # p.start()
        l.append(t)
        t.start()
    for t in l:
        t.join()
    # for p in l:
    #     p.join()
    stop = time.time()
    print(‘run time is :‘,(stop-start))

如果并发的多个任务是I/O密集型:多线程效率高

#IO密集型用多线程
from multiprocessing import Process
from threading import Thread
import os
import time
 
def work():
    time.sleep(0.5)
 
if  __name__ == ‘__main__‘:
    l = []
    print(os.cpu_count())
    start = time.time()
    for i in range(400):
        # p = Process(target=work)  #run time is : 39.320624113082886
        p = Thread(target=work)      #run time is : 0.5927295684814453
        l.append(p)
        p.start()
    for p in l:
        p.join()
    stop = time.time()
    print(‘run time is :‘,(stop-start))

应用:

多线程用于IO密集型,如socket 爬虫 ,web
 
多进程用于计算密集型,如金融分析

 

六,死锁现象

  所谓死锁就是指两个或者两个以上的进程或者线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,他们都将无法推进下去,此时称系统处于死锁状况或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

from threading import Thread,Lock
import time
mutexA=Lock()
mutexB=Lock()
 
class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print(‘33[41m%s 拿到A锁33[0m‘ %self.name)
 
        mutexB.acquire()
        print(‘33[42m%s 拿到B锁33[0m‘ %self.name)
        mutexB.release()
 
        mutexA.release()
 
    def func2(self):
        mutexB.acquire()
        print(‘33[43m%s 拿到B锁33[0m‘ %self.name)
        time.sleep(2)
 
        mutexA.acquire()
        print(‘33[44m%s 拿到A锁33[0m‘ %self.name)
        mutexA.release()
 
        mutexB.release()
 
if __name__ == ‘__main__‘:
    for i in range(10):
        t=MyThread()
        t.start()

  执行效果

Thread-1 拿到A锁
Thread-1 拿到B锁
Thread-1 拿到B锁
Thread-2 拿到A锁
 
 
 
 
#出现死锁,整个程序阻塞住

 

七,递归锁

  死锁的解决方法是是使用递归锁,递归锁,是在python中为了支持在同一线程中多次请求同一资源,python提供了可重入锁RLock

  这个RLock内部维护着一个Lock和一个counter变量,counter记录了acquire的次数,从而使得资源可以被多次require。直到一个线程所有的acquire都被release,其他的线程才能获得资源。上面的例子如果使用RLock代替Lock,则不会发生死锁,二者的区别是:递归锁可以连续acquire多次,而互斥锁只能acquire一次。

from threading import Thread,RLock
import time
 
mutexA=mutexB=RLock()
 #一个线程拿到锁,counter加1,该线程内又碰到加锁的情况,则counter继续加1,
#这期间所有其他线程都只能等待,等待该线程释放所有锁,即counter递减到0为止
 
class MyThread(Thread):
    def run(self):
        self.func1()
        self.func2()
    def func1(self):
        mutexA.acquire()
        print(‘33[41m%s 拿到A锁33[0m‘ %self.name)
 
        mutexB.acquire()
        print(‘33[42m%s 拿到B锁33[0m‘ %self.name)
        mutexB.release()
 
        mutexA.release()
 
    def func2(self):
        mutexB.acquire()
        print(‘33[43m%s 拿到B锁33[0m‘ %self.name)
        time.sleep(2)
 
        mutexA.acquire()
        print(‘33[44m%s 拿到A锁33[0m‘ %self.name)
        mutexA.release()
 
        mutexB.release()
 
if __name__ == ‘__main__‘:
    for i in range(10):
        t=MyThread()
        t.start()

  结果:

Thread-1 拿到了A锁
Thread-1 拿到了B锁
Thread-1 拿到了B锁
Thread-1 拿到了A锁
Thread-2 拿到了A锁
Thread-2 拿到了B锁
Thread-2 拿到了B锁
Thread-2 拿到了A锁
Thread-4 拿到了A锁
Thread-4 拿到了B锁
Thread-4 拿到了B锁
Thread-4 拿到了A锁
Thread-6 拿到了A锁
Thread-6 拿到了B锁
Thread-6 拿到了B锁
Thread-6 拿到了A锁
Thread-8 拿到了A锁
Thread-8 拿到了B锁
Thread-8 拿到了B锁
Thread-8 拿到了A锁
Thread-10 拿到了A锁
Thread-10 拿到了B锁
Thread-10 拿到了B锁
Thread-10 拿到了A锁
Thread-5 拿到了A锁
Thread-5 拿到了B锁
Thread-5 拿到了B锁
Thread-5 拿到了A锁
Thread-9 拿到了A锁
Thread-9 拿到了B锁
Thread-9 拿到了B锁
Thread-9 拿到了A锁
Thread-7 拿到了A锁
Thread-7 拿到了B锁
Thread-7 拿到了B锁
Thread-7 拿到了A锁
Thread-3 拿到了A锁
Thread-3 拿到了B锁
Thread-3 拿到了B锁
Thread-3 拿到了A锁

 

八,信号量

  信号量也是一把锁,可以指定信号量为5,对比互斥锁同一时间只能有一个任务抢到锁去执行,信号量同一时间可以有5个任务拿到锁去执行,如果说互斥锁是合租房屋的人去抢一个厕所,那么信号量就相当于一群路人争抢公共厕所,公共厕所有多个坑位,这意味着同一时间可以有多个人上公共厕所,但公共厕所容纳的人数是一定的,这便是信号量的大小

from threading import Thread,Semaphore
import threading
import time
 
def func():
    sm.acquire()
    print(‘%s get sm‘ %threading.current_thread().getName())
    time.sleep(3)
    sm.release()
 
if __name__ == ‘__main__‘:
    sm=Semaphore(5)
    for i in range(23):
        t=Thread(target=func)
        t.start()

  解析:

Semaphore管理一个内置的计数器,
每当调用acquire()时内置计数器-1;
调用release() 时内置计数器+1;
计数器不能小于0;当计数器为0时,acquire()将阻塞线程直到其他线程调用release()。

  与进程池是完全不同的概念,进程池Pool(4),最大只能产生4个进程,而且从头到尾都只是这四个进程,不会产生新的,而信号量是产生一堆线程/进程

 

九,Event

  同进程的一样

  线程的一个关键特性是每个线程都是独立运行且状态不可预测。如果程序中的其 他线程需要通过判断某个线程的状态来确定自己下一步的操作,这时线程同步问题就会变得非常棘手。为了解决这些问题,我们需要使用threading库中的Event对象。 对象包含一个可由线程设置的信号标志,它允许线程等待某些事件的发生。在 初始情况下,Event对象中的信号标志被设置为假。如果有线程等待一个Event对象, 而这个Event对象的标志为假,那么这个线程将会被一直阻塞直至该标志为真。一个线程如果将一个Event对象的信号标志设置为真,它将唤醒所有等待这个Event对象的线程。如果一个线程等待一个已经被设置为真的Event对象,那么它将忽略这个事件, 继续执行

event.isSet():返回event的状态值;
 
event.wait():如果 event.isSet()==False将阻塞线程;
 
event.set(): 设置event的状态值为True,所有阻塞池的线程激活进入就绪状态, 等待操作系统调度;
 
event.clear():恢复event的状态值为False。

技术图片

 技术图片

  例如,有多个工作线程尝试链接MySQL,我们想要在链接前确保MySQL服务正常才让那些工作线程去连接MySQL服务器,如果连接不成功,都会去尝试重新连接。那么我们就可以采用threading.Event机制来协调各个工作线程的连接操作

from threading import Thread,Event
import threading
import time,random
def conn_mysql():
    count=1
    while not event.is_set():
        if count > 3:
            raise TimeoutError(‘链接超时‘)
        print(‘<%s>第%s次尝试链接‘ % (threading.current_thread().getName(), count))
        event.wait(0.5)
        count+=1
    print(‘<%s>链接成功‘ %threading.current_thread().getName())
 
 
def check_mysql():
    print(‘33[45m[%s]正在检查mysql33[0m‘ % threading.current_thread().getName())
    time.sleep(random.randint(2,4))
    event.set()
if __name__ == ‘__main__‘:
    event=Event()
    conn1=Thread(target=conn_mysql)
    conn2=Thread(target=conn_mysql)
    check=Thread(target=check_mysql)
 
    conn1.start()
    conn2.start()
    check.start()

 

十,条件Condition(了解)

  使线程等待,只有满足了某条件时,才能释放n个线程。

import threading
  
def run(n):
    con.acquire()
    con.wait()
    print("run the thread: %s" %n)
    con.release()
  
if __name__ == ‘__main__‘:
  
    con = threading.Condition()
    for i in range(10):
        t = threading.Thread(target=run, args=(i,))
        t.start()
  
    while True:
        inp = input(‘>>>‘)
        if inp == ‘q‘:
            break
        con.acquire()
        con.notify(int(inp))
        con.release()

 

def condition_func():
 
    ret = False
    inp = input(‘>>>‘)
    if inp == ‘1‘:
        ret = True
 
    return ret
 
 
def run(n):
    con.acquire()
    con.wait_for(condition_func)
    print("run the thread: %s" %n)
    con.release()
 
if __name__ == ‘__main__‘:
 
    con = threading.Condition()
    for i in range(10):
        t = threading.Thread(target=run, args=(i,))
        t.start()

 

十一,定时器

  定时器指定n秒后执行某操作,比如定时炸弹

from threading import Timer
  
  
def hello():
    print("hello, world")
  
t = Timer(1, hello)
t.start()  # after 1 seconds, "hello, world" will be printed

  验证码定时器

from threading import Timer
import random,time
 
class Code:
    def __init__(self):
        self.make_cache()
 
    def make_cache(self,interval=5):
        self.cache=self.make_code()
        print(self.cache)
        self.t=Timer(interval,self.make_cache)
        self.t.start()
 
    def make_code(self,n=4):
        res=‘‘
        for i in range(n):
            s1=str(random.randint(0,9))
            s2=chr(random.randint(65,90))
            res+=random.choice([s1,s2])
        return res
 
    def check(self):
        while True:
            inp=input(‘>>: ‘).strip()
            if inp.upper() ==  self.cache:
                print(‘验证成功‘,end=‘
‘)
                self.t.cancel()
                break
 
 
if __name__ == ‘__main__‘:
    obj=Code()
    obj.check()

以上是关于Python GIL(Global Interpreter Lock)的主要内容,如果未能解决你的问题,请参考以下文章

Python GIL(Global Interpreter Lock)

Python GIL(Global Interpreter Lock)

Python GIL(Global Interpreter Lock)

python GIL(Global Interpreter Lock)

python-GIL(Global Interpreter Lock)

Python解释器是单线程应用 IO 密集型 计算密集型 GIL global interpreter lock