Python GIL(Global Interpreter Lock)

戰爭熱誠發表於2018-05-27

一,介紹

定義:
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('\033[41m%s 拿到A鎖\033[0m' %self.name)

        mutexB.acquire()
        print('\033[42m%s 拿到B鎖\033[0m' %self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('\033[43m%s 拿到B鎖\033[0m' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('\033[44m%s 拿到A鎖\033[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('\033[41m%s 拿到A鎖\033[0m' %self.name)

        mutexB.acquire()
        print('\033[42m%s 拿到B鎖\033[0m' %self.name)
        mutexB.release()

        mutexA.release()

    def func2(self):
        mutexB.acquire()
        print('\033[43m%s 拿到B鎖\033[0m' %self.name)
        time.sleep(2)

        mutexA.acquire()
        print('\033[44m%s 拿到A鎖\033[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('\033[45m[%s]正在檢查mysql\033[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='\n')
                self.t.cancel()
                break


if __name__ == '__main__':
    obj=Code()
    obj.check()

驗證碼定時器

  

 

相關文章