【并發(fā)操作】協(xié)程,線程,進(jìn)程是什么,在Python中怎么應(yīng)用?
前言
生活中的多任務(wù)時(shí)時(shí)刻刻存在,例如小張一邊碼字一邊看屏幕,又例如小蔡可以一邊跳舞一邊打籃球,這就是生活中的多任務(wù)。那么計(jì)算機(jī)中的多任務(wù)是什么呢、怎么使用呢?就讓我們一起探討計(jì)算機(jī)中,多任務(wù)-線程、多任務(wù)-進(jìn)程、多任務(wù)-協(xié)程的理解以及在Python中的應(yīng)用。

多任務(wù)
多任務(wù)處理是指用戶可以在同一時(shí)間內(nèi)進(jìn)行多種操作,每個(gè)操作被稱作一個(gè)任務(wù)。在計(jì)算機(jī)中,同時(shí)打開迅雷以及QQ是多任務(wù)同時(shí)進(jìn)行,在迅雷中看電影的時(shí)候,進(jìn)行邊下邊播也是多任務(wù),在同一時(shí)間同一單位進(jìn)行的不同操作,都可以理解為多任務(wù)。
現(xiàn)在多核CPU已經(jīng)非常普及了,但事實(shí)上,過去即便是單核CPU也可以執(zhí)行多任務(wù)。由于CPU執(zhí)行代碼都是順序執(zhí)行的,那單核CPU是怎么執(zhí)行多任務(wù)的呢?
答案就是操作系統(tǒng)輪流讓各個(gè)任務(wù)交替執(zhí)行,任務(wù)1執(zhí)行0.01秒,切換到任務(wù)2,任務(wù)2執(zhí)行0.01秒,再切換到任務(wù)3,……這樣反復(fù)執(zhí)行下去。表面上看,每個(gè)任務(wù)都是交替執(zhí)行的,但是,由于CPU的執(zhí)行速度實(shí)在是太快了,我們感覺就像所有任務(wù)都在同時(shí)執(zhí)行。
真正實(shí)現(xiàn)并行執(zhí)行多任務(wù)只能在多核CPU上實(shí)現(xiàn),但往往任務(wù)數(shù)量遠(yuǎn)遠(yuǎn)多于CPU的核心數(shù)量,所以操作系統(tǒng)也會(huì)自動(dòng)把很多任務(wù)輪流調(diào)度到每個(gè)核心上執(zhí)行。在這里我們引入并發(fā)與并行以及隊(duì)列的概念:
并發(fā):
cpu通過在任務(wù)間快速切換達(dá)到多任務(wù)一起執(zhí)行,但實(shí)際上并不是同時(shí)執(zhí)行,舉例:
A和B去跑步,跑道很擠只能容納一個(gè)人,兩人約定好每人跑一秒,大家都跑一秒就退出跑道。這時(shí),在同一時(shí)間內(nèi),總有一個(gè)人在跑道內(nèi)、一個(gè)人在跑道外(下圖中兩隊(duì)人排同一個(gè)咖啡機(jī)即為并發(fā))
并行:
每個(gè)任務(wù)都有不同cpu去執(zhí)行,達(dá)到多任務(wù)一起執(zhí)行,實(shí)際是真正的同時(shí)執(zhí)行,舉例還是A和B兩人去跑步。這次跑道升級(jí)了,有兩條跑道,A和B實(shí)現(xiàn)了并肩奔跑,你我互不影響(圖中兩隊(duì)人排兩臺(tái)咖啡機(jī)即為并行)
隊(duì)列:
就是一個(gè)有序的排列,在多任務(wù)中需要把待執(zhí)行的任務(wù)排好隊(duì),有序執(zhí)行。在A和B跑步的例子中,假設(shè)有20個(gè)A和20個(gè)B需要跑步,在排隊(duì)等待跑步的時(shí)候,形成的排列就稱為隊(duì)列(圖中兩個(gè)隊(duì)伍即為隊(duì)列)

思考:迅雷播放電影的同時(shí)用QQ聊天,和在迅雷中看電影邊下邊播多任務(wù)有什么不同?
01 線程
一個(gè)程序運(yùn)行起來至少有一個(gè)進(jìn)程,一個(gè)進(jìn)程至少有一個(gè)線程;
處理器cpu分配給線程,即cpu真正運(yùn)行的是線程中的代碼;
分配cpu給線程時(shí),是通過時(shí)間片輪訓(xùn)方式進(jìn)行的;
進(jìn)程是操作系統(tǒng)分配程序執(zhí)行資源的單位,而線程是進(jìn)程的一個(gè)實(shí)體;
是CPU調(diào)度和分配的單位。
在上述思考中,迅雷和QQ屬于不同的進(jìn)程,迅雷下載電影和播放電影屬于不同的線程,即一開始分配了兩份資源給迅雷和QQ,迅雷和QQ各為一個(gè)進(jìn)程。
當(dāng)你打開迅雷邊下邊播功能的時(shí)候,在迅雷這個(gè)進(jìn)程中又新開了兩個(gè)線程,不斷地在下載和播放間進(jìn)行切換,達(dá)到多任務(wù)的效果. ?線程與進(jìn)程是屬于關(guān)系。
線程由進(jìn)程創(chuàng)建,進(jìn)程結(jié)束線程也結(jié)束了,但線程結(jié)束進(jìn)程不一定結(jié)束,cpu最終分配給的是線程,而不是進(jìn)程。
線程執(zhí)行代碼片段原理:線程獲得cpu執(zhí)行內(nèi)存,執(zhí)行當(dāng)前代碼,在執(zhí)行另一個(gè)代碼塊之前打上時(shí)間戳,存儲(chǔ)上下文然后去執(zhí)行另一代碼塊。當(dāng)再次回到該代碼塊時(shí)加載時(shí)間戳,上下文,驗(yàn)證執(zhí)行的合理性,如此反復(fù)執(zhí)行下去,在不同的需要執(zhí)行的代碼塊間切換。

子線程何時(shí)開啟,何時(shí)運(yùn)行?
當(dāng)調(diào)用thread.start()時(shí) 開啟線程,再運(yùn)行線程的代碼。
子線程何時(shí)結(jié)束?
子線程把target指向的函數(shù)中的語句執(zhí)行完畢后,或者線程中的run函數(shù)代碼執(zhí)行完畢后,立即結(jié)束當(dāng)前子線程。
查看當(dāng)前線程數(shù)量
通過threading.enumerate()可枚舉當(dāng)前運(yùn)行的所有線程。
主線程何時(shí)結(jié)束?
所有子線程執(zhí)行完畢后,主線程才結(jié)束。
02 進(jìn)程
進(jìn)程:
一個(gè)程序運(yùn)行起來后,“代碼+用到的資源”稱之為進(jìn)程,它是操作系統(tǒng)分配資源的基本單位。
進(jìn)程狀態(tài):
工作中,任務(wù)數(shù)往往大于cpu的核數(shù),即一定有一些任務(wù)正在執(zhí)行,另外一些任務(wù)在等待cpu進(jìn)行執(zhí)行,因此導(dǎo)致了有了不同的狀態(tài)。

就緒態(tài):
運(yùn)行的條件都已經(jīng)滿足,正在等在cpu執(zhí)行。
執(zhí)行態(tài):
cpu正在執(zhí)行其功能。
等待態(tài):
等待某些條件滿足,例如一個(gè)程序sleep了,此時(shí)就處于等待態(tài),好比說:紅綠燈、等待消息回復(fù)、等待同步鎖都是處于等待態(tài)。
03 協(xié)程
協(xié)程是Python中另外一種實(shí)現(xiàn)多任務(wù)的方式,只不過比線程更小占用、執(zhí)行單元,由于協(xié)程是本世紀(jì)出現(xiàn)的新概念,所以對于協(xié)程來說沒有統(tǒng)一的概念,這里介紹我自己的理解,協(xié)程相當(dāng)于更便捷更輕量的線程。
協(xié)程與線程差異在于,實(shí)現(xiàn)多任務(wù)時(shí), 線程切換從系統(tǒng)層面遠(yuǎn)不止保存和恢復(fù) CPU上下文這么簡單。操作系統(tǒng)為了程序運(yùn)行的高效性,每個(gè)線程都有自己緩存Cache等數(shù)據(jù),操作系統(tǒng)還會(huì)幫你做這些數(shù)據(jù)的恢復(fù)操作。所以線程的切換非常耗性能。但是協(xié)程的切換只是單純操作CPU的上下文,所以一秒鐘切換個(gè)上百萬次系統(tǒng)都扛得住。
04 隊(duì)列與他們的關(guān)系
三者在工作時(shí)都需要取得cpu,為了避免彼此之間爭奪cpu,所以需要對他們進(jìn)行排隊(duì)處理,排好的隊(duì)伍就叫隊(duì)列,例如線程池、進(jìn)程池。

05 三者間的關(guān)系
進(jìn)程>線程>協(xié)程
線程由進(jìn)程創(chuàng)建,屬于進(jìn)程,協(xié)程是進(jìn)程更小程度的劃分,更輕便靈活,如下圖:


在Python中實(shí)現(xiàn)多任務(wù)
01 Python實(shí)現(xiàn)多線程

自定義類,繼承threading.Thread;
創(chuàng)建對象;
調(diào)用對象的run()方法。
02 Python實(shí)現(xiàn)多進(jìn)程

實(shí)例化一個(gè)對象 target= 指定到對應(yīng)的函數(shù);
調(diào)用對象的run()方法。
03 Python實(shí)現(xiàn)多協(xié)程

函數(shù)中調(diào)用yield;
調(diào)用yield后函數(shù)會(huì)在執(zhí)行到調(diào)用send() 方法結(jié)果返回時(shí)才繼續(xù)進(jìn)行下一步;
執(zhí)行函數(shù),函數(shù)會(huì)交替執(zhí)行。

多任務(wù)的痛點(diǎn)及解決方法
01 痛點(diǎn)1
如果多個(gè)線程同時(shí)對一個(gè)全局變量操作,會(huì)出現(xiàn)資源競爭問題,從而數(shù)據(jù)結(jié)果會(huì)不正確。

運(yùn)行上述代碼后會(huì)發(fā)現(xiàn)兩個(gè)線程對同一個(gè)數(shù)據(jù)操作完后得到的數(shù)據(jù)不一樣,這就是遇到了線程安全問題。
解決方法: 同步就是協(xié)同步調(diào),按預(yù)定的先后次序運(yùn)行。如:你說完,我再說;你做完,我再做;你執(zhí)行完,我再執(zhí)行。
在多線程編程中,一些敏感數(shù)據(jù)不允許被多個(gè)線程同時(shí)訪問,因?yàn)闀?huì)出現(xiàn)線程安全問題。通過線程同步機(jī)制,能保證共享數(shù)據(jù)在任何時(shí)刻,最多有一個(gè)線程訪問,以保證數(shù)據(jù)的正確性。線程同步提示的幾點(diǎn):
線程同步就是線程排隊(duì);
共享資源的讀寫才需要同步;
變量才需要同步,常量不需要同步;
給數(shù)據(jù)加鎖,即我操作完你再操作,你操作完我再操作。
threading模塊中定義了Lock類,可以方便的處理鎖定:



02 痛點(diǎn)2
GIL全局解釋器鎖:顧名思義,這是解釋器內(nèi)部的一把鎖,確切一點(diǎn)說是CPython解釋器內(nèi)部的一把鎖,所以要注意區(qū)分我們在Python代碼中使用的Lock不是一個(gè)層面的概念。言外之意,就是全局解釋器就是為了鎖定整個(gè)解釋器內(nèi)部的全局資源,每個(gè)線程想要運(yùn)行首先獲取GIL,而GIL本身又是一把互斥鎖,造成所有線程只能一個(gè)一個(gè)one-by-one-并發(fā)-交替的執(zhí)行。
也就是說Python中多線程并不能很好的實(shí)現(xiàn)并發(fā)操作,但Python恰好又是實(shí)現(xiàn)多協(xié)程的一種方法,所以對于Python來說,實(shí)現(xiàn)多任務(wù)最好的方式即為多進(jìn)程+多協(xié)程。
擴(kuò)展 celery原理
celery是基于Python實(shí)現(xiàn)的一個(gè)異步任務(wù)的調(diào)度工具,同時(shí)還是一個(gè)任務(wù)隊(duì)列,主要用于處理耗時(shí)的任務(wù)。
大家在使用celery的時(shí)候,都需要去配置一個(gè)隊(duì)列才能繼續(xù)使用,因?yàn)閷τ赾elery來說,也是一個(gè)生產(chǎn)者消費(fèi)者的模式,我們一般使用的隊(duì)列是Redis或者RabbitMQ,因?yàn)榇鎯?chǔ)格式為鍵值對形式,序號(hào)對應(yīng)任務(wù),利于cpu執(zhí)行。celery即為消息中間件,任務(wù)執(zhí)行單元,任務(wù)執(zhí)行結(jié)果儲(chǔ)存的形式進(jìn)行異步操作,如圖:


總結(jié)
請看如下例子: 有一個(gè)老板想要開個(gè)工廠進(jìn)行生產(chǎn)剪子,他需要花一些財(cái)力物力制作一條生產(chǎn)線,這個(gè)生產(chǎn)線上有很多的器件以及材料這些所有的,為了能夠生產(chǎn)剪子而準(zhǔn)備的資源稱之為:進(jìn)程
只有生產(chǎn)線是不能夠進(jìn)行生產(chǎn)的,所以老板的找個(gè)工人來進(jìn)行生產(chǎn),這個(gè)工人能夠利用這些材料最終一步步的將剪子做出來,這個(gè)來做事情的工人稱之為:線程
這個(gè)老板為了提高生產(chǎn)率,想到3種辦法:
在這條生產(chǎn)線上多招些工人,一起來做剪子,這樣效率是成倍増長,即單進(jìn)程 多線程方式
老板發(fā)現(xiàn)這條生產(chǎn)線上的工人不是越多越好,因?yàn)橐粭l生產(chǎn)線的資源以及材料畢竟有限,所以老板又花了些財(cái)力物力購置了另外一條生產(chǎn)線,然后再招些工人這樣效率又再一步提高了,即多進(jìn)程 多線程方式。
老板發(fā)現(xiàn),現(xiàn)在已經(jīng)有了很多條生產(chǎn)線,并且每條生產(chǎn)線上已經(jīng)有很多工人了(即程序是多進(jìn)程的,每個(gè)進(jìn)程中又有多個(gè)線程),為了再次提高效率,老板想了個(gè)損招,規(guī)定:如果某個(gè)員工在上班時(shí)臨時(shí)沒事或者再等待某些條件(比如等待另一個(gè)工人生產(chǎn)完某道工序 之后他才能再次工作) ,那么這個(gè)員工就利用這個(gè)時(shí)間去做其它的事情,那么也就是說:如果一個(gè)線程等待某些條件,可以充分利用這個(gè)時(shí)間去做其它事情,其實(shí)這就是:協(xié)程方式。
簡單來說:
1. 進(jìn)程是操作系統(tǒng)資源分配的單位;
2. 線程是CPU調(diào)度的單位;
3. 進(jìn)程切換需要的資源最大,效率很低;
4. 線程切換需要的資源一般,效率一般(當(dāng)然在不考慮GIL的情況下);
5. 協(xié)程切換任務(wù)資源很小,效率高;
6. 多進(jìn)程、多線程根據(jù)cpu核數(shù)不一樣可能是并行的,但是協(xié)程是在一個(gè)線程中所以是并發(fā)。

藍(lán)鯨智云
本文由騰訊藍(lán)鯨智云編輯發(fā)布,騰訊藍(lán)鯨智云(簡稱藍(lán)鯨)軟件體系是一套基于PaaS的技術(shù)解決方案,致力于打造行業(yè)領(lǐng)先的一站式自動(dòng)化運(yùn)維平臺(tái)。目前已經(jīng)推出社區(qū)版、企業(yè)版,歡迎體驗(yàn)。
官網(wǎng):https://bk.tencent.com/
下載鏈接:https://bk.tencent.com/download/
社區(qū):https://bk.tencent.com/s-mart/community/question