Python3 GIL(Global Interpreter Lock)與多線程


GIL(Global Interpreter Lock)與多線程

  1. GIL介紹

  2. GIL與Lock

  3. GIL與多線程

  4. 多線程性能測試

 

在Cpython解釋器中,同一個進程下開啟的多線程,同一時刻只能有一個線程執行,無法利用多核優勢。

GIL並不是Python的特性,他是在實現Python解釋器(Cpython)時所引入的一個概念,因為Cpython是大部分環境下默認的Python執行環境。

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

一 GIL介紹

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

 有了GIL的存在,同一時刻同一進程中只有一個線程被執行(無法利用多核)

 GIL保護的是解釋器級的數據,保護用戶自己的數據則需要自己加鎖處理

 

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

'''
#驗證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只會產生一個進程

在一個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

GIL保護的是解釋器級的數據,保護用戶自己的數據則需要自己加鎖處理,如下圖

 

三 GIL與多線程

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

進程可以利用多核,但是開銷大,而python的多線程開銷小,但卻無法利用多核優勢,也就是說python沒用了?

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

#1. cpu到底是用來做計算的,還是用來做I/O的?

#2. 多cpu,意味着可以有多個核並行完成計算,所以多核提升的是計算性能

#3. 每個cpu一旦遇到I/O阻塞,仍然需要等待,所以多核對I/O操作沒什么用處 

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

  當然對運行一個程序來說,隨着cpu的增多執行效率肯定會有所提高(不管提高幅度多大,總會有所提高),這是因為一個程序基本上不會是純計算或者純I/O,所以我們只能相對的去看一個程序到底是計算密集型還是I/O密集型,從而進一步分析python的多線程到底有無用武之地

比如:

  在IO密集型的情況運行四個任務:1、開四個進程在四個CPU上並行運行,執行完四個任務的時間為最長的那個任務執行的時間

                 2、開四個線程在同一個CPU上並發運行,執行完四個任務的時間也是為最長的那個執行任務的時間。

                 甚至,多線程執行完的時間比多進程的還要慢,因為創建一個線程比創建一個進程要快的多(進程需要向操作系統申請內存空間,放入一系列相關資源)

#分析:
我們有四個任務需要處理,處理方式肯定是要玩出並發的效果,解決方案可以是:
方案一:開啟四個進程
方案二:一個進程下,開啟四個線程

#單核情況下,分析結果: 
  如果四個任務是計算密集型,沒有多核來並行計算,方案一徒增了創建進程的開銷,方案二勝
  如果四個任務是I/O密集型,方案一創建進程的開銷大,且進程的切換速度遠不如線程,方案二勝

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

 
#結論:現在的計算機基本上都是多核,python對於計算密集型的任務開多線程的效率並不能帶來多大性能上的提升,甚至不如串行(沒有大量切換),但是,對於IO密集型的任務效率還是有顯著提升的。

 

 

四 多線程性能測試

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))
計算密集型:多進程效率高
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))

I/O密集型:多線程效率高
IO密集型:多線程效率高

 

應用:

多線程用於IO密集型,如socket,爬蟲,web
多進程用於計算密集型,如金融分析


免責聲明!

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



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