第48天:初識 Python 多線程

圖片

咱們知道,多線程與單線程相比,能夠提升 CPU 利用率,加快程序的響應速度。html

單線程是按順序執行的,好比用單線程執行以下操做:python

6秒讀取文件19秒處理文件15秒讀取文件28秒處理文件2

總共用時 28 秒,若是開啓兩條線程來執行上面的操做(假設處理器爲多核 CPU),以下所示:編程

6秒讀取文件1 + 5秒讀取文件29秒處理文件1 + 8秒處理文件2

只需 15 秒就可完成。安全

1 線程與進程

1.1 簡介

說到線程就不得不提與之相關的另外一概念:進程,那麼什麼是進程?與線程有什麼關係呢?簡單來講一個運行着的應用程序就是一個進程,好比:我啓動了本身手機上的酷貓音樂播放器,這就是一個進程,而後我隨意點了一首歌曲進行播放,此時酷貓啓動了一條線程進行音樂播放,聽了一部分,我感受歌曲還不錯,因而我按下了下載按鈕,此時酷貓又啓動了一條線程進行音樂下載,如今酷貓同時進行着音樂播放和音樂下載,此時就出現了多線程,音樂播放線程與音樂下載線程並行運行,說到並行,你必定想到了併發吧,那並行與併發有什麼區別呢?並行強調的是同一時刻,併發強調的是一段時間內。線程是進程的一個執行單元,一個進程中至少有一條線程,進程是資源分配的最小單位,線程是  CPU 調度的最小單位。網絡

線程通常會經歷新建(New)、就緒(Runnable)、運行(Running)、阻塞(Blocked)、死亡(Dead)5  種狀態,當線程被建立並啓動後,並不會直接進入運行狀態,也不會一直處於運行狀態,CPU  可能會在多個線程之間切換,線程的狀態也會在就緒和運行之間轉換。多線程

1.2 Python 中的線程與進程

Python  提供了 _thread(Python3 以前名爲 thread ) 和 threading 兩個線程模塊。_thread  是低級、原始的模塊,threading 是高級模塊,對 _thread 進行了封裝,加強了其功能與易用性,絕大多數時候,咱們只需使用  threading 模塊便可。下一節咱們會對 threading 模塊進行詳細介紹。併發

Python  提供了 multiprocessing 模塊對多進程進行支持,它使用了與 threading 模塊類似的 API  產生進程,除此以外,還增長了新的 API,用於支持跨多個輸入值並行化函數的執行及跨進程分配輸入數據,詳細用法能夠參考官方文檔  https://docs.python.org/zh-cn/3/library/multiprocessing.html。app

2 GIL

要說 Python 的多線程,必然繞不開 GIL,可謂成也 GIL 敗也 GIL,到底 GIL 是啥?怎麼來的?爲何說成也 GIL 敗也 GIL 呢?下面就帶着這幾個問題,給你們介紹一下 GIL。框架

2.1 GIL 相關概念

GIL 全稱 Global Interpreter Lock(全局解釋器鎖),是 Python 解釋器 CPython 採用的一種機制,經過該機制來控制同一時刻只有一條線程執行 Python 字節碼,本質是一把全局互斥鎖,將並行運行變成串行運行。編程語言

什麼是  CPython 呢?咱們從 Python 官方網站下載安裝 Python 後,得到的官方解釋器就是 CPython,因其是 C  語言開發的,故名爲 CPython,是目前使用最普遍的 Python 解釋器;由於咱們大部分環境下使用的默認解釋器就是  CPython,有些人會認爲 CPython 就是 Python,進而覺得 GIL 是 Python 的特性,其實 CPython 只是一種  Python 解釋器,除了 CPython 解釋器還有:PyPy、Psyco、Jython (也稱 JPython)、IronPython  等解釋器,其中 Jython 與 IronPython 分別採用 Java 與 C# 語言實現,就沒有采用 GIL 機制;而 GIL 也不是  Python 特性,Python 能夠徹底獨立於 GIL 運行。

2.2 GIL 起源與發展

咱們已經知道了 GIL 是 CPython 解釋器中引入的機制,那爲何 CPython 解釋器中要引入 GIL 呢?GIL 一開始出現是由於 CPython 解釋器的內存管理不是線程安全的,也就是採用 GIL 這把鎖解決 CPython 的線程安全問題。

隨着時間的推移,計算機硬件逐漸向多核多線程方向發展,爲了更加充分的利用多核  CPU 資源,各類編程語言開始對多線程進行支持,Python  也加入了其中,儘管多線程的編程方式能夠提升程序的運行效率,但與此同時也帶來了線程間數據一致性和狀態同步的問題,解決這個問題最簡單的方式就是加鎖,因而  GIL 這把鎖再次登場,很容易便解決了這個問題。

慢慢的愈來愈多的代碼庫開發者開始接受了這種設定,進而開始大量依賴這種特性,由於默認加了 GIL 後,Python 的多線程即是線程安全的了,開發者在實際開發無需再考慮線程安全問題,省掉了很多麻煩。

對於 CPython 解釋器中的多線程程序,爲了保證多線程操做安全,默認使用了 GIL 鎖,保證任意時刻只有一個線程在執行,其餘線程處於等待狀態。

2.3 成也 GIL,敗也 GIL

之前爲了解決多線程的線程操做安全問題,CPython 採用了 GIL 鎖的方式,這種方式雖然解決了線程操做安全問題,但因爲同一時刻只能有一條線程執行,等於主動放棄了線程並行執行的機會,所以在目前 CPython 下的多線程並非真正意義上的多線程。

如今這種狀況,咱們可能會想要實現真正意義上的多線程,可不能夠去掉 GIL 呢?答案是能夠的,可是有一個問題:依賴這個特性的代碼庫太多了,如今已是尾大不掉了,使去除 GIL 的工做變得舉步維艱。

當初爲了解決多線程帶來的線程操做安全問題使用了 GIL,如今又發現 GIL 方式下的多線程比較低效,想要去掉 GIL,但已經到了尾大不掉的地步了,真是成也 GIL,敗也 GIL。

對於 CPython 下多線程的低效問題,除了去掉 GIL,還有什麼其餘解決方案嗎?咱們來簡單瞭解下:

1)使用無 GIL 機制的解釋器;如:Jython 與 IronPython,但使用這兩個解釋器失去了利用 C 語言模塊一些優秀特性的機會,所以這種方式仍是比較小衆。

2)使用  multiprocess 代替 threading;multiprocess 使用了與 threading 模塊類似的 API  產生進程,不一樣之處是它使用了多進程而不是多線程,每一個進程有本身獨立的 GIL,所以不會出現進程之間的 GIL  爭搶,但這種方式只對計算密集型任務有效,經過後面的示例咱們也能得出這個結論。

3 多線程實現

_thread   模塊是一個底層模塊,功能較少,當主線程運行完畢後,若是不作任何處理,會馬上把子線程給結束掉,現實中幾乎不多使用該模塊,所以不做過多介紹。對於多線程開發推薦使用  threading 模塊,這裏咱們簡單瞭解下經過該模塊實現多線程,詳細介紹咱們放在了下一節多線程的文章中。

threading 模塊經過 Thread 類提供對多線程的支持,首先,咱們要導入 threading 中的類 Thread,示例以下:

from threading import Thread

依賴導入了,接下來要就要建立線程了,直接建立 Thread 實例便可,示例以下:

# method 爲線程要執行的具體方法p1 = Thread(target=method)

若要實現兩條線程,再建立一個 Thread 實例便可,示例以下:

p2 = Thread(target=method)

須要實現更多條的線程也是一個道理。線程建立好了,經過 start 方法啓動便可,示例以下:

p1.start()p2.start()

若是是多線程任務,咱們可能須要等待全部線程執行完成再進行下一步操做,使用 join 方法便可。示例以下:

# 等待線程 p一、p2 都執行完p1.join()p2.join()

4 多進程實現

Python 的多進程經過 multiprocessing 模塊的 Process 類實現,它的使用基本與 threading 模塊的 Thread 類一致,所以這裏就不一步步說了,直接看示例:

# 導入 Processfrom multiprocessing import Process
# 建立兩個進程實例:p一、p2,method 是要執行的具體方法p1 = Process(target=method)p2 = Process(target=method)
# 啓動兩個進程p1.start()p2.start()
# 等待進程 p一、p2 都執行完p1.join()p2.join()

5 效率大比拼

如今咱們已經瞭解了 Python 線程和進程的基本使用,那麼 Python 單線程、多線程、多進程的實際工做效率如何呢?下面咱們就以計算密集型和 I/O 密集型兩種任務考驗一下它們。

5.1 計算密集型任務

計算密集型任務的特色是要進行大量的計算,消耗 CPU 資源,好比:計算圓周率、對視頻進行解碼 ... 全靠 CPU 的運算能力,下面看一下單線程、多線程、多進程的實際耗時狀況。

1)單線程就是一條線程,咱們直接以主線程爲例,來看下單線程表現如何:

# 計算密集型任務-單線程import os,time
def task():    ret = 0    for i in range(100000000):        ret *= iif __name__ == '__main__':    print('本機爲',os.cpu_count(),'核 CPU')    start = time.time()    for i in range(5):        task()    stop = time.time()    print('單程耗時 %s' % (stop - start))# 測試結果:'''本機爲 4 核 CPU單線程耗時 23.19068455696106'''

2)來看多線程表現:

# 計算密集型任務-多線程from threading import Threadimport os,time
def task():    ret = 0    for i in range(100000000):        ret *= iif __name__ == '__main__':    arr = []    print('本機爲',os.cpu_count(),'核 CPU')    start = time.time()    for i in range(5):        p = Thread(target=task)        arr.append(p)        p.start()    for p in arr:        p.join()    stop = time.time()    print('多線程耗時 %s' % (stop - start))# 測試結果:'''本機爲 4 核 CPU多線程耗時 25.024707317352295'''

3)來看多進程表現:

# 計算密集型任務-多進程from multiprocessing import Processimport os,time
def task():    ret = 0    for i in range(100000000):        ret *= iif __name__ == '__main__':    arr = []    print('本機爲',os.cpu_count(),'核 CPU')    start = time.time()    for i in range(5):        p = Process(target=task)        arr.append(p)        p.start()    for p in arr:        p.join()    stop = time.time()    print('計算密集型任務,多進程耗時 %s' % (stop - start))# 輸出結果'''本機爲 4 核 CPU計算密集型任務,多進程耗時 14.087027311325073'''

經過測試結果咱們發現,在 CPython 下執行計算密集型任務時,多進程效率最優,多線程還不如單線程。

5.2 I/O 密集型任務

涉及到網絡、磁盤 I/O 的任務都是 I/O 密集型任務,這類任務的特色是 CPU 消耗不多,任務的大部分時間都在等待 I/O 操做完成(由於 I/O 的速度遠遠低於 CPU 和內存的速度)。經過下面例子看一下耗時狀況:

1)來看單線程表現:

# I/O 密集型任務-單線程import os,time
def task():    f = open('tmp.txt','w')if __name__ == '__main__':    arr = []    print('本機爲',os.cpu_count(),'核 CPU')    start = time.time()    for i in range(500):        task()    stop = time.time()    print('I/O 密集型任務,多進程耗時 %s' % (stop - start))# 輸出結果'''本機爲 4 核 CPUI/O 密集型任務,單線程耗時 0.2964005470275879'''

2)來看多線程表現:

# I/O 密集型任務-多線程from threading import Threadimport os,time
def task():    f = open('tmp.txt','w')if __name__ == '__main__':    arr = []    print('本機爲',os.cpu_count(),'核 CPU')    start = time.time()    for i in range(500):        p = Thread(target=task)        arr.append(p)        p.start()    for p in arr:        p.join()    stop = time.time()    print('I/O 密集型任務,多進程耗時 %s' % (stop - start))# 輸出結果'''本機爲 4 核 CPUI/O 密集型任務,多線程耗時 0.24960064888000488'''

3)來看多進程表現:

# I/O 密集型任務-多進程from multiprocessing import Processimport os,time
def task():    f = open('tmp.txt','w')if __name__ == '__main__':    arr = []    print('本機爲',os.cpu_count(),'核 CPU')    start = time.time()    for i in range(500):        p = Process(target=task)        arr.append(p)        p.start()    for p in arr:        p.join()    stop = time.time()    print('I/O 密集型任務,多進程耗時 %s' % (stop - start))# 輸出結果''' 本機爲 4 核 CPUI/O 密集型任務,多進程耗時 21.05265736579895'''

經過 I/O 密集型任務在 CPython 下的測試結果咱們發現:多線程效率優於多進程,單線程與多線程效率接近。

對於一個運行的程序來講,隨着 CPU 的增長執行效率必然會有所提升,所以大多數時候,一個程序不會是純計算或純 I/O,因此咱們只能相對的去看一個程序是計算密集型仍是 I/O 密集型。

總結

本節給你們介紹了 Python 多線程,讓你們對 Python 多線程現狀有了必定了解,可以根據任務類型選擇更加高效的處理方式。

示例代碼:Python-100-days-day048

參考:

https://www.cnblogs.com/SuKiWX/p/8804974.html

https://docs.python.org/zh-cn/3/glossary.html#term-global-interpreter-lock

系列文章

    第47天:Web 開發 RESTful

    第46天:Flask數據持久化

    第45天:Web表單

    第44天:Flask 框架集成Bootstrap

    第43天:Python filecmp&difflib模塊

    第42天:Python paramiko 模塊

    第41天:Python operator 模塊

    第0-40天:從0學習Python 0-40合集
相關文章
相關標籤/搜索