生活中的多任務時時刻刻存在,例如小張一邊碼字一邊看屏幕,又例如小蔡可以一邊跳舞一邊打籃球,這就是生活中的多任務。那麼計算機中的多任務是什麼呢、怎麼使用呢?就讓我們一起探討計算機中,多任務-線程、多任務-進程、多任務-協程的理解以及在python中的應用。
多任務
多任務處理是指用戶可以在同一時間內進行多種操作,每個操作被稱作一個任務。在計算機中,同時打開迅雷以及QQ是多任務同時進行,在迅雷中看電影的時候,進行邊下邊播也是多任務,在同一時間同一單位進行的不同操作,都可以理解爲多任務。
現在多核CPU已經非常普及了,但事實上,過去即便是單核CPU也可以執行多任務。由於CPU執行代碼都是順序執行的,那單核CPU是怎麼執行多任務的呢?
答案就是操作系統輪流讓各個任務交替執行,任務1執行0.01秒,切換到任務2,任務2執行0.01秒,再切換到任務3,……這樣反覆執行下去。表面上看,每個任務都是交替執行的,但是,由於CPU的執行速度實在是太快了,我們感覺就像所有任務都在同時執行。
真正實現並行執行多任務只能在多核CPU上實現,但往往任務數量遠遠多於CPU的核心數量,所以操作系統也會自動把很多任務輪流調度到每個核心上執行。在這裏我們引入併發與並行以及隊列的概念:
併發:
cpu通過在任務間快速切換達到多任務一起執行,但實際上並不是同時執行,舉例:
A和B去跑步,跑道很擠只能容納一個人,兩人約定好每人跑一秒,大家都跑一秒就退出跑道。這時,在同一時間內,總有一個人在跑道內、一個人在跑道外(下圖中兩隊人排同一個咖啡機即爲併發)
並行:
每個任務都有不同cpu去執行,達到多任務一起執行,實際是真正的同時執行,舉例還是A和B兩人去跑步。這次跑道升級了,有兩條跑道,A和B實現了並肩奔跑,你我互不影響(圖中兩隊人排兩臺咖啡機即爲並行)
隊列:
就是一個有序的排列,在多任務中需要把待執行的任務排好隊,有序執行。在A和B跑步的例子中,假設有20個A和20個B需要跑步,在排隊等待跑步的時候,形成的排列就稱爲隊列(圖中兩個隊伍即爲隊列)
▲ 併發並行與隊列
思考:迅雷播放電影的同時用QQ聊天,和在迅雷中看電影邊下邊播多任務有什麼不同?
1、線程
- 一個程序運行起來至少有一個進程,一個進程至少有一個線程;
- 處理器cpu分配給線程,即cpu真正運行的是線程中的代碼;
- 分配cpu給線程時,是通過時間片輪訓方式進行的;
- 進程是操作系統分配程序執行資源的單位,而線程是進程的一個實體;
- 是CPU調度和分配的單位。
在上述思考中,迅雷和QQ屬於不同的進程,迅雷下載電影和播放電影屬於不同的線程,即一開始分配了兩份資源給迅雷和QQ,迅雷和QQ各爲一個進程。
當你打開迅雷邊下邊播功能的時候,在迅雷這個進程中又新開了兩個線程,不斷地在下載和播放間進行切換,達到多任務的效果. 線程與進程是屬於關係。
線程由進程創建,進程結束線程也結束了,但線程結束進程不一定結束,cpu最終分配給的是線程,而不是進程。
線程執行代碼片段原理:線程獲得cpu執行內存,執行當前代碼,在執行另一個代碼塊之前打上時間戳,存儲上下文然後去執行另一代碼塊。當再次回到該代碼塊時加載時間戳,上下文,驗證執行的合理性,如此反覆執行下去,在不同的需要執行的代碼塊間切換。
▲進程與線程間的關係
子線程何時開啓,何時運行?
當調用thread.start()時 開啓線程,再運行線程的代碼。
子線程何時結束?
子線程把target指向的函數中的語句執行完畢後,或者線程中的run函數代碼執行完畢後,立即結束當前子線程。
查看當前線程數量
通過threading.enumerate()可枚舉當前運行的所有線程。
主線程何時結束?
所有子線程執行完畢後,主線程才結束。
2、進程
進程:
一個程序運行起來後,代碼+用到的資源 稱之爲進程,它是操作系統分配資源的基本單位。
進程狀態:
工作中,任務數往往大於cpu的核數,即一定有一些任務正在執行,而另外一些任務在等待cpu進行執行,因此導致了有了不同的狀態。
就緒態:
運行的條件都已經滿足,正在等在cpu執行。
執行態:
cpu正在執行其功能。
等待態:
等待某些條件滿足,例如一個程序sleep了,此時就處於等待態,好比說:紅綠燈、等待消息回覆、等待同步鎖都是處於等待態。
3、協程
協程是python中另外一種實現多任務的方式,只不過比線程更小佔用、執行單元,由於協程是本世紀出現的新概念,所以對於協程來說沒有統一的概念,這裏介紹我自己的理解,協程相當於更便捷更輕量的線程。
協程與線程差異在於,實現多任務時, 線程切換從系統層面遠不止保存和恢復 CPU上下文這麼簡單。操作系統爲了程序運行的高效性,每個線程都有自己緩存Cache等數據,操作系統還會幫你做這些數據的恢復操作。所以線程的切換非常耗性能。但是協程的切換隻是單純操作CPU的上下文,所以一秒鐘切換個上百萬次系統都扛得住。
4、隊列與他們的關係
三者在工作時都需要取得cpu,爲了避免彼此之間爭奪cpu,所以需要對他們進行排隊處理,排好的隊伍就叫隊列,例如線程池、進程池。
▲隊列的原理
5、三者間的關係
進程>線程>協程
線程由進程創建,屬於進程,協程是進程更小程度的劃分,更輕便靈活,如下圖:
▲ 進程線程協程三者間的關係
在python中實現多任務
1、Python實現多線程
- 自定義類,繼承threading.Thread;
- 創建對象;
- 調用對象的run()方法。
2、Python實現多進程
- 實例化一個對象 target= 指定到對應的函數;
- 調用對象的run()方法。
3、Python實現多協程
- 函數中調用yield;
- 調用yield後函數會在執行到調用send() 方法結果返回時才繼續進行下一步;
- 執行函數,函數會交替執行。
多任務的痛點及解決方法
1、痛點1
如果多個線程同時對同一個全局變量操作,會出現資源競爭問題,從而數據結果會不正確。
運行上述代碼後會發現兩個線程對同一個數據操作完後得到的數據不一樣,這就是遇到了線程安全問題。
解決方法:
同步就是協同步調,按預定的先後次序運行。如:你說完,我再說;你做完,我再做;你執行完,我再執行。
在多線程編程中,一些敏感數據不允許被多個線程同時訪問,因爲會出現線程安全問題。通過線程同步機制,能保證共享數據在任何時刻,最多有一個線程訪問,以保證數據的正確性。線程同步提示的幾點:
- 線程同步就是線程排隊;
- 共享資源的讀寫才需要同步;
- 變量才需要同步,常量不需要同步;
- 給數據加鎖,即我操作完你再操作,你操作完我再操作。
threading模塊中定義了Lock類,可以方便的處理鎖定:
2、痛點2
GIL全局解釋器鎖:顧名思義,這是解釋器內部的一把鎖,確切一點說是CPython解釋器內部的一把鎖,所以要注意區分我們在Python代碼中使用的Lock不是一個層面的概念。言外之意,就是全局解釋器就是爲了鎖定整個解釋器內部的全局資源,每個線程想要運行首先獲取GIL,而GIL本身又是一把互斥鎖,造成所有線程只能一個一個one-by-one-併發-交替的執行。
也就是說python中多線程並不能很好的實現併發操作,但python恰好又是實現多協程的一種方法,所以對於python來說,實現多任務最好的方式即爲多進程+多協程。
擴展 celery原理
celery是基於python實現的一個異步任務的調度工具,同時還是一個任務隊列,主要用於處理耗時的任務。
大家在使用celery的時候,都需要去配置一個隊列才能繼續使用,因爲對於celery來說,也是一個生產者消費者的模式,我們一般使用的隊列是Redis或者RabbitMQ,因爲存儲格式爲鍵值對形式,序號對應任務,利於cpu執行。celery即爲消息中間件,任務執行單元,任務執行結果儲存的形式進行異步操作,如圖:
總結
請看如下例子: 有一個老闆想要開個工廠進行生產剪子,他需要花一些財力物力製作一條生產線,這個生產線上有很多的器件以及材料這些所有的,爲了能夠生產剪子而準備的資源稱之爲:進程
只有生產線是不能夠進行生產的,所以老闆的找個工人來進行生產,這個工人能夠利用這些材料最終一步步的將剪子做出來,這個來做事情的工人稱之爲:線程
這個老闆爲了提高生產率,想到3種辦法:
- 在這條生產線上多招些工人,一起來做剪子,這樣效率是成倍増長,即單進程 多線程方式。
- 老闆發現這條生產線上的工人不是越多越好,因爲一條生產線的資源以及材料畢竟有限,所以老闆又花了些財力物力購置了另外一條生產線,然後再招些工人這樣效率又再一步提高了,即多進程 多線程方式。
- 老闆發現,現在已經有了很多條生產線,並且每條生產線上已經有很多工人了(即程序是多進程的,每個進程中又有多個線程),爲了再次提高效率,老闆想了個損招,規定:如果某個員工在上班時臨時沒事或者再等待某些條件(比如等待另一個工人生產完某道工序 之後他才能再次工作) ,那麼這個員工就利用這個時間去做其它的事情,那麼也就是說:如果一個線程等待某些條件,可以充分利用這個時間去做其它事情,其實這就是:協程方式。
簡單來說:
- 進程是操作系統資源分配的單位;
- 線程是CPU調度的單位;
- 進程切換需要的資源最大,效率很低;
- 線程切換需要的資源一般,效率一般(當然在不考慮GIL的情況下);
- 協程切換任務資源很小,效率高;
- 多進程、多線程根據cpu核數不一樣可能是並行的,但是協程是在一個線程中所以是併發。