網絡編程之多線程——GIL全局解釋器鎖

網絡編程之多線程——GIL全局解釋器鎖

1、引子

定義:
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。php

2、GIL介紹

GIL本質就是一把互斥鎖,既然是互斥鎖,全部互斥鎖的本質都同樣,都是將併發運行變成串行,以此來控制同一時間內共享數據只能被一個任務所修改,進而保證數據安全。python

能夠確定的一點是:保護不一樣的數據的安全,就應該加不一樣的鎖。linux

要想了解GIL,首先肯定一點:每次執行python程序,都會產生一個獨立的進程。例如python test.py,python aaa.py,python bbb.py會產生3個不一樣的python進程web

驗證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的主線程或者由該主線程開啓的其餘線程,還有解釋器開啓的垃圾回收等解釋器級別的線程,總之,全部線程都運行在這一個進程內,毫無疑問。windows

一、全部數據都是共享的,這其中,代碼做爲一種數據也是被全部線程共享的(test.py的全部代碼以及Cpython解釋器的全部代碼)
例如:test.py定義一個函數work(代碼內容以下圖),在進程內全部線程都能訪問到work的代碼,因而咱們能夠開啓三個線程而後target都指向該代碼,能訪問到意味着就是能夠執行。

二、全部線程的任務,都須要將任務的代碼當作參數傳給解釋器的代碼去執行,即全部的線程要想運行本身的任務,首先須要解決的是可以訪問到解釋器的代碼。

綜上:安全

若是多個線程的target=work,那麼執行流程是網絡

多個線程先訪問到解釋器的代碼,即拿到執行權限,而後將target的代碼交給解釋器的代碼去執行多線程

解釋器的代碼是全部線程共享的,因此垃圾回收線程也可能訪問到解釋器的代碼而去執行,這就致使了一個問題:對於同一個數據100,可能線程1執行x=100的同時,而垃圾回收執行的是回收100的操做,解決這種問題沒有什麼高明的方法,就是加鎖處理,以下圖的GIL,保證python解釋器同一時間只能執行一個任務的代碼。併發

3、GIL與Lock

機智的同窗可能會問到這個問題:Python已經有一個GIL來保證同一時間只能有一個線程來執行了,爲何這裏還須要lock?

首先,咱們須要達成共識:鎖的目的是爲了保護共享的數據,同一時間只能有一個線程來修改共享的數據

而後,咱們能夠得出結論:保護不一樣的數據就應該加不一樣的鎖。

最後,問題就很明朗了,GIL 與Lock是兩把鎖,保護的數據不同,前者是解釋器級別的(固然保護的就是解釋器級別的數據,好比垃圾回收的數據),後者是保護用戶本身開發的應用程序的數據,很明顯GIL不負責這件事,只能用戶自定義加鎖處理,即Lock,以下圖:

分析:

一、100個線程去搶GIL鎖,即搶執行權限
二、確定有一個線程先搶到GIL(暫且稱爲線程1),而後開始執行,一旦執行就會拿到lock.acquire()
三、極有可能線程1還未運行完畢,就有另一個線程2搶到GIL,而後開始運行,但線程2發現互斥鎖lock還未被線程1釋放,因而阻塞,被迫交出執行權限,即釋放GIL
四、直到線程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

4、GIL與多線程

有了GIL的存在,同一時刻同一進程中只有一個線程被執行。

聽到這裏,有的同窗立馬質問:進程能夠利用多核,可是開銷大,而python的多線程開銷小,但卻沒法利用多核優點,也就是說python沒用了,php纔是最牛逼的語言?

彆着急,還沒講完呢。

要解決這個問題,咱們須要在幾個點上達成一致:

一、cpu究竟是用來作計算的,仍是用來作I/O的?
二、多cpu,意味着能夠有多個核並行完成計算,因此多核提高的是計算性能
三、每一個cpu一旦遇到I/O阻塞,仍然須要等待,因此多核對I/O操做沒什麼用處

一個工人至關於cpu,此時計算至關於工人在幹活,I/O阻塞至關於爲工人幹活提供所需原材料的過程,工人幹活的過程當中若是沒有原材料了,則工人幹活的過程須要中止,直到等待原材料的到來。

若是你的工廠乾的大多數任務都要有準備原材料的過程(I/O密集型),那麼你有再多的工人,意義也不大,還不如一我的,在等材料的過程當中讓工人去幹別的活。

反過來說,若是你的工廠原材料都齊全,那固然是工人越多,效率越高

結論:

一、對計算來講,cpu越多越好,可是對於I/O來講,再多的cpu也沒用
二、固然對運行一個程序來講,隨着cpu的增多執行效率確定會有所提升(無論提升幅度多大,總會有所提升),這是由於一個程序基本上不會是純計算或者純I/O,因此咱們只能相對的去看一個程序究竟是計算密集型仍是I/O密集型,從而進一步分析python的多線程到底有無用武之地

假設咱們有四個任務須要處理,處理方式確定是要玩出併發的效果,解決方案能夠是:

方案一:開啓四個進程
方案二:一個進程下,開啓四個線程

單核狀況下,分析結果:

一、若是四個任務是計算密集型,多核意味着並行計算,在python中一個進程中同一時刻只有一個線程執行用不上多核,方案一勝
二、若是四個任務是I/O密集型,再多的核也解決不了I/O問題,方案二勝

結論:

如今的計算機基本上都是多核,python對於計算密集型的任務開多線程的效率並不能帶來多大性能上的提高,甚至不如串行(沒有大量切換),可是,對於IO密集型的任務效率仍是有顯著提高的。

5、多線程性能測試

若是併發的多個任務是計算密集型:多進程效率高

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

應用:

一、多線程用於IO密集型,如socket,爬蟲,web
二、多進程用於計算密集型,如金融分析
相關文章
相關標籤/搜索