本文始發於我的公衆號:TechFlow,原創不易,求個關注程序員
今天是Python專題第20篇文章,咱們來聊聊Python當中的多線程。web
其實關於元類還有不少種用法,好比說如何在元類當中設置參數啦,以及一些規約的用法等等。只不過這些用法比較小衆,使用頻率很是低,因此咱們不過多闡述了,能夠在用到的時候再去詳細瞭解。我想只要你們理解了元類的原理以及使用方法,再去學習那些具體的用法應該會很容易。因此咱們今天開始了一個新的話題——多線程和併發。後端
爲了照顧小白,咱們來簡單聊聊進程和線程這兩個概念。這兩個概念屬於操做系統,咱們常常據說,可是可能不多有人會細究它們的含義。對於工程師而言,二者的定義和區別仍是頗有必要了解清楚的。多線程
首先說進程,進程能夠當作是CPU執行的具體的任務。在操做系統當中,因爲CPU的運行速度很是快,要比計算機當中的其餘設備要快得多。好比內存、磁盤等等,因此若是CPU一次只執行一個任務,那麼會致使CPU大量時間在等待這些設備,這樣操做效率很低。爲了提高計算機的運行效率,把機器的技能儘量壓榨出來,CPU是輪詢工做的。也就是說它一次只執行一個任務,執行一小段碎片時間以後當即切換,去執行其餘任務。併發
因此在早期的單核機器的時候,看起來電腦也是併發工做的。咱們能夠一邊聽歌一邊上網,也不會以爲卡頓。但實際上,這是CPU輪詢的結果。在這個例子當中,聽歌的軟件和上網的軟件對於CPU而言都是獨立的進程。咱們能夠把進程簡單地理解成運行的應用,好比在安卓手機裏面,一個app啓動的時候就會對應系統中的一個進程。固然這種說法不徹底準確,一個應用也是能夠啓動多個進程的。app
進程是對應CPU而言的,線程則更多針對的是程序。即便是CPU在執行當前進程的時候,程序運行的任務其實也是有分工的。舉個例子,好比聽歌軟件當中,咱們須要顯示歌詞的字幕,須要播放聲音,須要監聽用戶的行爲,好比是否發生了切歌、調節音量等等。因此,咱們須要進一步拆分CPU的工做,讓它在執行當前進程的時候,繼續經過輪詢的方式來同時作多件事情。編輯器
進程中的任務就是線程,因此從這點上來講,進程和線程是包含關係。一個進程當中能夠包含多個線程,對於CPU而言,不能直接執行線程,一個線程必定屬於一個進程。因此咱們知道,CPU進程切換切換的是執行的應用程序或者是軟件,而進程內部的線程切換,切換的是軟件當中具體的執行任務。函數
關於進程和線程有一個經典的模型能夠說明它們之間的關係,假設CPU是一家工廠,工廠當中有多個車間。不一樣的車間對應不一樣的生產任務,有的車間生產汽車輪胎,有的車間生產汽車骨架。可是工廠的電力是有限的,同時只能知足一個廠房的使用。oop
爲了讓你們的進度協調,因此工廠個須要輪流提供各個車間的供電。這裏的車間對應的就是進程。學習
一個車間雖然只生產一種產品,可是其中的工序卻不止一個。一個車間可能會有好幾條流水線,具體的生產任務實際上是流水線完成的,每一條流水線對應一個具體執行的任務。可是一樣的,車間同一時刻也只能執行一條流水線,因此咱們須要車間在這些流水線之間切換供電,讓各個流水線生產進度統一。
這裏車間裏的流水線天然對應的就是線程的概念,這個模型很好地詮釋了CPU、進程和線程之間的關係。實際的原理也的確如此,不過CPU中的狀況要比現實中的車間複雜得多。由於對於進程和CPU來講,它們面臨的局面都是實時變化的。車間當中的流水線是x個,下一刻可能就成了y個。
瞭解完了線程和進程的概念以後,對於理解電腦的配置也有幫助。好比咱們買電腦,常常會碰到一個術語,就是這個電腦的CPU是某某核某某線程的。好比我當年買的第一臺筆記本是4核8線程的,這實際上是在說這臺電腦的CPU有4個計算核心,可是使用了超線程技術,使得能夠把一個物理核心模擬成兩個邏輯核心。至關於咱們能夠用4個核心同時執行8個線程,至關於8個核心同時執行,但其實有4個核心是模擬出來的虛擬核心。
有一個問題是爲何是4核8線程而不是4核8進程呢?由於CPU並不會直接執行進程,而是執行的是進程當中的某一個線程。就好像車間並不能直接生產零件,只有流水線才能生產零件。車間負責的更可能是資源的調配,因此教科書裏有一句很是經典的話來詮釋:進程是資源分配的最小單元,線程是CPU調度的最小單元。
Python當中爲咱們提供了完善的threading庫,經過它,咱們能夠很是方便地建立線程來執行多線程。
首先,咱們引入threading中的Thread,這是一個線程的類,咱們能夠經過建立一個線程的實例來執行多線程。
from threading import Thread
t = Thread(target=func, name='therad', args=(x, y)) t.start() 複製代碼
簡單解釋一下它的用法,咱們傳入了三個參數,分別是target,name和args,從名字上咱們就能夠猜想出它們的含義。首先是target,它傳入的是一個方法,也就是咱們但願多線程執行的方法。name是咱們爲這個新建立的線程起的名字,這個參數能夠省略,若是省略的話,系統會爲它起一個系統名。當咱們執行Python的時候啓動的線程名叫MainThread,經過線程的名字咱們能夠作區分。args是會傳遞給target這個函數的參數。
咱們來舉個經典的例子:
import time, threading
# 新線程執行的代碼: def loop(n): print('thread %s is running...' % threading.current_thread().name) for i in range(n): print('thread %s >>> %s' % (threading.current_thread().name, i)) time.sleep(5) print('thread %s ended.' % threading.current_thread().name) print('thread %s is running...' % threading.current_thread().name) t = threading.Thread(target=loop, name='LoopThread', args=(10, )) t.start() print('thread %s ended.' % threading.current_thread().name) 複製代碼
咱們建立了一個很是簡單的loop函數,用來執行一個循環來打印數字,咱們每次打印一個數字以後這個線程會睡眠5秒鐘,因此咱們看到的結果應該是每過5秒鐘屏幕上多出一行數字。
咱們在Jupyter裏執行一下:
表面上看這個結果沒毛病,可是其實有一個問題,什麼問題呢?輸出的順序不太對,爲何咱們在打印了第一個數字0以後,主線程就結束了呢?另一個問題是,既然主線程已經結束了,爲何Python進程沒有結束, 還在向外打印結果呢?
由於線程之間是獨立的,對於主線程而言,它在執行了t.start()以後,並不會停留,而是會一直往下執行一直到結束。若是咱們不但願主線程在這個時候結束,而是阻塞等待子線程運行結束以後再繼續運行,咱們能夠在代碼當中加上t.join()這一行來實現這點。
t.start()
t.join() print('thread %s ended.' % threading.current_thread().name) 複製代碼
join操做可讓主線程在join處掛起等待,直到子線程執行結束以後,再繼續往下執行。咱們加上了join以後的運行結果是這樣的:
這個就是咱們預期的樣子了,等待子線程執行結束以後再繼續。
咱們再來看第二個問題,爲何主線程結束的時候,子線程還在繼續運行,Python進程沒有退出呢?這是由於默認狀況下咱們建立的都是用戶級線程,對於進程而言,會等待全部用戶級線程執行結束以後才退出。這裏就有了一個問題,那假如咱們建立了一個線程嘗試從一個接口當中獲取數據,因爲接口一直沒有返回,當前進程豈不是會永遠等待下去?
這顯然是不合理的,因此爲了解決這個問題,咱們能夠把建立出來的線程設置成守護線程。
守護線程即daemon線程,它的英文直譯實際上是後臺駐留程序,因此咱們也能夠理解成後臺線程,這樣更方便理解。daemon線程和用戶線程級別不一樣,進程不會主動等待daemon線程的執行,當全部用戶級線程執行結束以後即會退出。進程退出時會kill掉全部守護線程。
咱們傳入daemon=True參數來將建立出來的線程設置成後臺線程:
t = threading.Thread(target=loop, name='LoopThread', args=(10, ), daemon=True)
複製代碼
這樣咱們再執行看到的結果就是這樣了:
這裏有一點須要注意,若是你在jupyter當中運行是看不到這樣的結果的。由於jupyter自身是一個進程,對於jupyter當中的cell而言,它一直是有用戶級線程存活的,因此進程不會退出。因此想要看到這樣的效果,只能經過命令行執行Python文件。
若是咱們想要等待這個子線程結束,就必須經過join方法。另外,爲了預防子線程鎖死一直沒法退出的狀況, 咱們還能夠在joih當中設置timeout,即最長等待時間,當等待時間到達以後,將再也不等待。
好比我在join當中設置的timeout等於5時,屏幕上就只會輸出5個數字。
另外,若是沒有設置成後臺線程的話,設置timeout雖然也有用,可是進程仍然會等待全部子線程結束。因此屏幕上的輸出結果會是這樣的:
雖然主線程繼續往下執行而且結束了,可是子線程仍然一直運行,直到子線程也運行結束。
關於join設置timeout這裏有一個坑,若是咱們只有一個線程要等待還好,若是有多個線程,咱們用一個循環將它們設置等待的話。那麼主線程一共會等待N * timeout的時間,這裏的N是線程的數量。由於每一個線程計算是否超時的開始時間是上一個線程超時結束的時間,它會等待全部線程都超時,纔會一塊兒終止它們。
好比我這樣建立3個線程:
ths = []
for i in range(3): t = threading.Thread(target=loop, name='LoopThread' + str(i), args=(10, ), daemon=True) ths.append(t) for t in ths: t.start() for t in ths: t.join(2) 複製代碼
最後屏幕上輸出的結果是這樣的:
全部線程都存活了6秒,不得不說,這個設計有點坑,和咱們預想的徹底不同。
在今天的文章當中,咱們一塊兒簡單瞭解了操做系統當中線程和進程的概念,以及Python當中如何建立一個線程,以及關於建立線程以後的相關使用。今天介紹的只是最基礎的使用和概念,關於線程還有不少高端的用法,咱們將在後續的文章當中和你們分享。
多線程在許多語言當中都是相當重要的,許多場景下一定會使用到多線程。好比web後端,好比爬蟲,再好比遊戲開發以及其餘全部須要涉及開發ui界面的領域。由於凡是涉及到ui,必然會須要一個線程單獨渲染頁面,另外的線程負責準備數據和執行邏輯。所以,多線程是專業程序員繞不開的一個話題,也是必定要掌握的內容之一。
今天的文章就到這裏,若是喜歡本文,能夠的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。
本文使用 mdnice 排版