~~並發編程(十一):GIL全局解釋鎖~~


進擊のpython

*****

並發編程——GIL全局解釋鎖


這小節就是有些“大神”批判python語言不完美之處的開始

這一節我們要了解一下Cpython的GIL解釋器鎖的工作機制

掌握一下GIL和互斥鎖

最后再了解一下Cpython下多線程和多進程各自的應用場景

首先需要明確的一點就是GIL不是Python的特性

他是實現Python解釋器(Cpython)時所引入的一個概念

當然Python不止這一個解釋器來編譯代碼

只是因為Cpython是大部分默認環境下的Python執行環境

所以在很多人的概念里CPython就是Python

也就想當然的把GIL歸結為Python語言的缺陷

所以這里要先明確一點:GIL並不是Python的特性,Python完全可以不依賴於GIL


GIL介紹

其實GIL本質上就是一把互斥鎖,既然是互斥鎖,那么所有的互斥鎖本質都一樣的

都是將並發編程變成串行,以此來控制同一時間內共享數據只能被一個任務所修改

進而來保證數據的安全,可以肯定的一點是:保護不同數據的安全,就應該加不同的鎖

要想了解GIL,首先可以肯定一點的就是:每次執行一個py文件,都會產生一個獨立的進程

比如運行1.py 2.py 3.py 就會開三個進程,而且是開三個不同的進程

在一個python的進程中,不僅是有主線程,還應該有開啟的其他線程,比如垃圾回收機制級別的線程

但是這些線程都是在這個進程當中運行的,這個無需多言

而前面我們也提到了,線程之間的數據是共享的,既然數據是共享的

代碼,其實本身也是數據,也是被所有線程共享的,這其中也包括解釋器的代碼

而程序在執行之前,需要先執行編譯器的代碼(這很好理解,否則你的代碼僅僅是字符串)

那執行編譯器的代碼是不是也需要保證編譯器的代碼安全

所以為了保證代碼的安全,我們在編譯器上加了一把鎖,這把鎖就是GIL全局解釋鎖

而加了這把鎖就意味着什么?就意味着python解釋器同一時間只能執行一個任務代碼

這樣就不會出現垃圾回收代碼和用戶代碼同時操作一個變量導致邏輯混亂的問題


GIL與Lock

那既然都已經有一把鎖,來保證多線程只能一個一個運行的狀態

那為什么還要有Lock這個方法呢?有過這個疑問嗎?

還是那句話,加鎖的目的是為了保護共享的數據,保證同一時間只能有一個線程來修改共享的數據

進而我們就應該得出結論:保護不同的數據就應該加不同的鎖

那問題就變得很清晰了,GIL和Lock是兩把鎖,保護的數據是不一樣的

前者保護的是解釋器級別的代碼,比如垃圾回收機制啊什么的

但是后面的則是保護的自己開發的應用程序的數據,GIL是不負責的

只能用戶自己定義然后加鎖處理

如果有100個線程來搶GIL鎖

一定有一個線程A先搶到了GIL,然后就開始執行了,只要執行就會拿到lock.acquire()

很可能在A還沒運行完,另一個線程B搶到了GIL鎖,然后開始運行,看到lock沒有被釋放,於是就進行阻塞

阻塞的同時就會被迫交出GIL,直到A重新搶到GIL,從上次暫停的位置繼續執行,直到正常釋放互斥鎖lock

舉個例子吧:

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):
        t = Thread(target=work)
        l.append(t)
        t.start()
    for t in l:
        t.join()
    print(n)

打印的結果一定是 0 因為共享數據被保護了,只能一個一個執行


GIL與多線程

問題又出現了,進程呢,是可以利用多核,但是時間長,開銷大

python的多線程開銷是小,但是由於GIL的原因,不能利用多核優勢

這就是在小節剛開始提的批判的‘不完美’之處

在解決這個問題之前,應該對一些問題達成共識!

CPU到底是干啥的呢?是用來計算的還是用來處理I/O阻塞的呢?

很明顯是處理計算的,多個CPU是用來處理多個計算任務,換句話說,多個CPU是提高計算速度

但是當CPU遇到I/O阻塞的時候,還是需要等待的,所以,多CPU對處理阻塞沒什么用

如果你的工廠是處理石材的,那工人越多效率越快(MC玩多了)

但是,如果你是等待石材過來再加工的,那等待的過程,有多少工人也沒用

工人就是CPU,第一個例子就是計算密集型!第二個例子是I/O密集型!

從上面就可以看出來

對計算來說,CPU越多越好,但是對於I/O來說,再多的CPU也沒用

但是,沒有純計算和純I/O的程序,所以我們只能相對的去看一個程序到底是什么類型

所以解決問題是這樣的:

方案一:開啟多進程

方案二:開啟多線程

單核

如果是計算密集型,沒有多核的來並行計算,方案一增加了創建進程的開銷

如果是I/O密集型,方案一創建的進程開銷大,所以還是要選擇方案二

多核

如果是計算密集型,在python中同一時刻只能一個線程執行,用不到多核,所以應該選擇方案一

如果是I/O密集型,核就沒用了,所以應該用方案二

但是很明顯現在的計算機都是多核,python對於計算密集型的任務開多線程並不能提高效率

甚至有時候都比不上串行,但是要是對於I/O密集型任務,還是有顯著提升的


性能測試

上面說的這么熱鬧,下面就來親自試驗一下

1.如果並發的多個任務是計算密集型:多進程效率高

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))

如果並發的多個任務是I/O密集型:多線程效率高

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))
  1. 多線程用於IO密集型,如socket,爬蟲,web
  2. 多進程用於計算密集型,如金融分析

*****
*****


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM