python 协程可以嵌套协程吗_Python线程、协程探究(2)——揭开协程的神秘面纱...
一、上集回顧
在上一篇中我們主要研究了python的多線程困境,發(fā)現(xiàn)多核情況下由于GIL的存在,python的多線程程序無(wú)法發(fā)揮多線程該有的并行威力。在文章的結(jié)尾,我們提出如下需求: 既然python的多線程只是實(shí)現(xiàn)了并發(fā)功能,那么我們是否能夠進(jìn)一步的提升并發(fā)的能力,減小多線程的切換開(kāi)銷以及避免應(yīng)對(duì)多線程復(fù)雜的同步問(wèn)題?那么一個(gè)較好的解決方案就是我們本篇要介紹的協(xié)程技術(shù)。本篇仍然主要注重理論知識(shí)介紹,不著重講python的協(xié)程代碼實(shí)現(xiàn)。
大龍:Python線程、協(xié)程探究(1)——Python的多線程困境?zhuanlan.zhihu.com首先我們放個(gè)圖在這里,表示進(jìn)程、線程、協(xié)程的關(guān)系,圖片涉及的內(nèi)容會(huì)后續(xù)介紹。
進(jìn)程、線程、協(xié)程的關(guān)系
二、前景知識(shí)
協(xié)程并不是一個(gè)新的概念,事實(shí)上,協(xié)程的概念比線程提出來(lái)的還要早,協(xié)程涉及到的知識(shí)也不是新的知識(shí),所以介紹協(xié)程之前,我們首先明確一些基礎(chǔ)知識(shí),包括并發(fā)和并行的概念以及了解線程調(diào)度的相關(guān)概念。
并發(fā)和并行,虛線和實(shí)線代表兩個(gè)不同的任務(wù)
2.1 并發(fā)
計(jì)算機(jī)中每一個(gè)線程都是一個(gè)執(zhí)行任務(wù),假設(shè)我們現(xiàn)在有一個(gè)單核的CPU,CPU每時(shí)每刻只能調(diào)度執(zhí)行一個(gè)線程,我們第一種做法就是讓所有的線程排好隊(duì),一個(gè)任務(wù)一個(gè)任務(wù)的依次執(zhí)行,執(zhí)行完一個(gè)執(zhí)行下一個(gè)。采用這種方式的調(diào)度帶來(lái)的問(wèn)題就是,如果當(dāng)前執(zhí)行的任務(wù)陷入了死循環(huán),那么CPU會(huì)一直卡在這個(gè)任務(wù)上,導(dǎo)致后續(xù)的任務(wù)無(wú)法執(zhí)行。所以,操作系統(tǒng)采用的方案是,每個(gè)任務(wù)分一個(gè)時(shí)間片來(lái)執(zhí)行,時(shí)間片結(jié)束之后便切換任務(wù),換另一個(gè)執(zhí)行,做到雨露均沾。假設(shè)我們有4個(gè)任務(wù),每個(gè)任務(wù)都分250ms進(jìn)行計(jì)算,那么1s后,每個(gè)任務(wù)的擁有者都發(fā)現(xiàn)自己的任務(wù)往前進(jìn)行了一點(diǎn),這就是我們提到的并發(fā)(concurrency)。在POSIX中,并發(fā)的定義要求“延遲調(diào)用線程的函數(shù)不應(yīng)該導(dǎo)致其他線程的無(wú)限期延遲”。我們上面的四個(gè)任務(wù)中,并發(fā)操作之間可能任意交錯(cuò),對(duì)任務(wù)的擁有者來(lái)說(shuō),1s后四個(gè)任務(wù)都往前推進(jìn)了一部分,好像四個(gè)任務(wù)是并行執(zhí)行的,但是實(shí)際CPU執(zhí)行任務(wù)的時(shí)候還是一個(gè)一個(gè)執(zhí)行的,所以并發(fā)不代表操作同時(shí)進(jìn)行。那么如果我有四個(gè)核心的CPU會(huì)怎么樣呢,4個(gè)CPU核心會(huì)各自拿一個(gè)任務(wù)執(zhí)行,這種情況才是我們常說(shuō)的并行。
2.2 并行
并行只在多處理器的情況下才存在,因?yàn)槊總€(gè)處理器可以各自執(zhí)行一個(gè)任務(wù),這時(shí)四個(gè)任務(wù)便是并行執(zhí)行的。單處理器的情況下是沒(méi)辦法做到并行的。所以我們回顧中會(huì)說(shuō),即使在多核的CPU計(jì)算資源情況下,python的多線程沒(méi)有達(dá)到并行而只能達(dá)到并發(fā),因?yàn)槎鄠€(gè)線程無(wú)法同時(shí)被執(zhí)行,只能擊鼓傳花似的被依次的執(zhí)行。
2.3 線程調(diào)度——上下文切換
線程上下文切換
前文提到,為了實(shí)現(xiàn)并發(fā),我們需要讓CPU交替切換的執(zhí)行不同的任務(wù),但當(dāng)操作系統(tǒng)從thread1切換到thread2的時(shí)候,操作系統(tǒng)實(shí)際上打斷了thread1的執(zhí)行流程,那么下一次thread1重新被執(zhí)行的時(shí)候,怎么能保證是繼續(xù)上一次被打斷的時(shí)候的位置繼續(xù)執(zhí)行的呢?所以切換的時(shí)候要保存任務(wù)的執(zhí)行環(huán)境信息,比如代碼運(yùn)行到哪一行了,哪些變量被賦值了,當(dāng)時(shí)寄存器都是那些值等等。保存當(dāng)前線程的執(zhí)行環(huán)境信息,加載下一個(gè)線程的執(zhí)行環(huán)境的操作就稱為上下文切換。有了上下文切換,我們就不用擔(dān)心任務(wù)被打斷后會(huì)丟失一些執(zhí)行信息導(dǎo)致下一次接著執(zhí)行的時(shí)候出錯(cuò)。
2.4 線程調(diào)度——阻塞調(diào)用
當(dāng)運(yùn)行中的線程調(diào)用sleep操作時(shí),被阻塞,操作系統(tǒng)調(diào)度其他程序,直到該線程獲得喚醒信號(hào)
CPU是非常稀缺的計(jì)算資源,每一納秒都是珍貴的,所以我們調(diào)度任務(wù)的目標(biāo)就是讓CPU不停的去計(jì)算,別讓它空閑著。當(dāng)線程A中的代碼調(diào)用了文件讀取操作時(shí),會(huì)發(fā)生什么呢?
def由于存儲(chǔ)的訪問(wèn)速度非常慢,CPU就會(huì)原地空轉(zhuǎn)一直等著DMA把數(shù)據(jù)準(zhǔn)備好,準(zhǔn)備好了之后再往下執(zhí)行。那么CPU等待的這段時(shí)間就完全被空閑浪費(fèi)了,因?yàn)镃PU等待的時(shí)候還有其他的任務(wù)迫切的需要任務(wù)計(jì)算。所以操作系統(tǒng)選擇當(dāng)線程A調(diào)用文件讀取這樣的阻塞操作的時(shí)候,就把線程A阻塞掛起,停止執(zhí)行線程A,然后調(diào)度另一個(gè)線程繼續(xù)執(zhí)行,當(dāng)線程A需要的數(shù)據(jù)準(zhǔn)備好了之后,操作系統(tǒng)便會(huì)在未來(lái)的某個(gè)時(shí)刻調(diào)度線程A繼續(xù)執(zhí)行,如果線程A的數(shù)據(jù)始終都準(zhǔn)備不好,那么線程A就永遠(yuǎn)不會(huì)被調(diào)度執(zhí)行。
三、協(xié)程理解
協(xié)程是用戶級(jí)的線程,是線程之上的輕量級(jí)線程
有了前面的基礎(chǔ)知識(shí),我們理解協(xié)程就會(huì)簡(jiǎn)單很多,事實(shí)上,協(xié)程本質(zhì)就是用戶態(tài)下的線程,進(jìn)程里的線程的切換調(diào)度是由操作系統(tǒng)來(lái)負(fù)責(zé)的。但是線程內(nèi)的協(xié)程的調(diào)度執(zhí)行,是由線程來(lái)負(fù)責(zé)的。如果我們把協(xié)程對(duì)應(yīng)到原生線程,那么協(xié)程所在的原生線程就是操作系統(tǒng)的角色。即原生線程需要負(fù)責(zé)什么時(shí)候切換協(xié)程,什么時(shí)候掛起協(xié)程。協(xié)程切換的時(shí)候,線程需要把協(xié)程A的執(zhí)行環(huán)境進(jìn)行保存,在下一次執(zhí)行A的時(shí)候,線程需要恢復(fù)執(zhí)行環(huán)境,這樣就可以從A之前的位置繼續(xù)執(zhí)行。
用戶線程即為協(xié)程,操作系統(tǒng)感知不到協(xié)程的存在,只調(diào)度內(nèi)核線程
在這里我們需要提醒的是,多線程的使用是可以讓一個(gè)程序獲得更多的計(jì)算時(shí)間的,但是協(xié)程的使用不會(huì), 多線程的使用在多核的情況下,可以達(dá)到并行的效果,但是協(xié)程的使用不會(huì)達(dá)到并行的效果。因?yàn)椴僮飨到y(tǒng)感知不到協(xié)程的存在,只會(huì)把時(shí)間片和CPU核心分給線程。至于分給線程的時(shí)間,線程又會(huì)分配給哪個(gè)協(xié)程來(lái)運(yùn)行,那是線程自己決定的內(nèi)容。比如分配2ms給一個(gè)擁有兩個(gè)協(xié)程的線程A,線程被操作系統(tǒng)調(diào)度指派給了CPU核心C1, A會(huì)決定在C1運(yùn)行哪個(gè)線程,,可以雨露均沾,讓兩個(gè)協(xié)程各自運(yùn)行1ms, 也可以是把2ms全部分配給一個(gè)協(xié)程,自始至終,所有的協(xié)程都運(yùn)行在CPU核心C1上,所以無(wú)法實(shí)現(xiàn)協(xié)程并行。
線程內(nèi)部自主進(jìn)行協(xié)程調(diào)度
那使用協(xié)程的好處是什么呢?提高線程的并發(fā)度,減小切換的開(kāi)銷,限于篇幅,這里就不展開(kāi)講,其結(jié)論就是,協(xié)程的切換只是線程棧內(nèi)的切換操作,不涉及內(nèi)核操作,其切換速度遠(yuǎn)快于線程。
如果我們要實(shí)現(xiàn)協(xié)程調(diào)度,我們?cè)搶?shí)現(xiàn)哪些功能呢。比如有一個(gè)線程底下有兩個(gè)協(xié)程A,B,根據(jù)用戶輸入的文件名,A協(xié)程進(jìn)行文件讀取,并返回文件內(nèi)容,B協(xié)程根據(jù)文件名計(jì)算哈希值并返回。
# 以下代碼并非真實(shí)的python協(xié)程代碼,只是為了說(shuō)明例子線程首先調(diào)度執(zhí)行A,執(zhí)行到文件讀取部分發(fā)現(xiàn)需要等待,于是掛起協(xié)程A并切換到協(xié)程B執(zhí)行。所以要實(shí)現(xiàn)調(diào)度協(xié)程,那么至少需要實(shí)現(xiàn)協(xié)程掛起操作和協(xié)程恢復(fù)運(yùn)行兩個(gè)操作, 如果不想手動(dòng)進(jìn)行調(diào)度,那么可以實(shí)現(xiàn)一個(gè)中央的調(diào)度器來(lái)幫助進(jìn)行調(diào)度。
四、協(xié)程的實(shí)現(xiàn)
協(xié)程主要有如下兩個(gè)特點(diǎn):
- 協(xié)程可以保留運(yùn)行時(shí)的狀態(tài)數(shù)據(jù)
- 協(xié)程可以出讓自己的執(zhí)行權(quán),當(dāng)重新獲得執(zhí)行權(quán)時(shí)從上一次暫停的位置繼續(xù)執(zhí)行
保留運(yùn)行時(shí)狀態(tài)數(shù)據(jù)就是上下文切換時(shí)做的工作,便于下一次執(zhí)行時(shí)能繼續(xù)上一次暫停的位置執(zhí)行。協(xié)程出讓執(zhí)行權(quán),指的是如果線程指定一個(gè)協(xié)程運(yùn)行,除非該協(xié)程主動(dòng)放棄執(zhí)行權(quán),不然線程無(wú)法將協(xié)程掛起切換。
Lua很早就有了語(yǔ)言級(jí)別對(duì)協(xié)程的實(shí)現(xiàn),我個(gè)人覺(jué)得其協(xié)程API還是比較清晰的, 在這里簡(jiǎn)單介紹說(shuō)明一下。
Lua中關(guān)于協(xié)程的API五、Talk is cheap, show me the code
python的協(xié)程實(shí)現(xiàn)歷史較為悠久,很多介紹協(xié)程的文章會(huì)從很早的協(xié)程庫(kù)開(kāi)始介紹,因?yàn)楸酒┛透鄬W⒂趨f(xié)程的概念理解,并不專注于python的協(xié)程技術(shù)實(shí)現(xiàn),我們就直接從最新的協(xié)程代碼編寫方式開(kāi)始介紹。
python3.4之后引入了asyncio模塊,使得協(xié)程的使用更加的方便,其中關(guān)鍵詞async表明這一塊函數(shù)是一個(gè)協(xié)程塊,而不是普通的函數(shù)模塊(函數(shù)模塊從中間退出之后,是不會(huì)保留運(yùn)行環(huán)境的,但是協(xié)程會(huì)保留), await關(guān)鍵字表明協(xié)程主動(dòng)出讓執(zhí)行權(quán)。我們定義三個(gè)協(xié)程模塊,并讓調(diào)度器進(jìn)行調(diào)度執(zhí)行A和B。首先調(diào)度運(yùn)行協(xié)程B, 運(yùn)行到sleep函數(shù)的時(shí)候遇到await關(guān)鍵字并出讓執(zhí)行權(quán),這時(shí)調(diào)度器切換執(zhí)行協(xié)程A,協(xié)程A執(zhí)行又遇到await,再一次出讓執(zhí)行權(quán)。這時(shí)兩個(gè)協(xié)程都在等待喚醒的信號(hào)。等待到了信號(hào)之后,兩個(gè)協(xié)程被喚醒進(jìn)而調(diào)度執(zhí)行,然后運(yùn)行結(jié)束。結(jié)果如下
import程序結(jié)果1:
協(xié)程B開(kāi)始執(zhí)行 協(xié)程B出讓執(zhí)行權(quán) 協(xié)程A開(kāi)始執(zhí)行 協(xié)程A出讓執(zhí)行權(quán) 協(xié)程B重新獲得執(zhí)行權(quán),并執(zhí)行結(jié)束 協(xié)程A重新獲得執(zhí)行權(quán),并執(zhí)行結(jié)束 程序運(yùn)行時(shí)間: 2.002208709716797此時(shí)我們加上第三個(gè)協(xié)程進(jìn)行調(diào)度,這樣當(dāng)A、B等待時(shí)鐘信號(hào)的時(shí)候我們?cè)诘却钠陂g,讓調(diào)度器執(zhí)行調(diào)度協(xié)程C,雖然協(xié)程C也調(diào)用sleep函數(shù),但是由于睡眠時(shí)間短,所以很快又會(huì)被喚醒進(jìn)行調(diào)度執(zhí)行。當(dāng)然了,由于協(xié)程C是死循環(huán),所以協(xié)程A、B結(jié)束之后,會(huì)一直執(zhí)行協(xié)程C。
import程序運(yùn)行部分結(jié)果:
協(xié)程B開(kāi)始執(zhí)行 協(xié)程B出讓執(zhí)行權(quán) 協(xié)程A開(kāi)始執(zhí)行 協(xié)程A出讓執(zhí)行權(quán) 由于協(xié)程A,B始終等待時(shí)鐘信號(hào),協(xié)程C執(zhí)行 由于協(xié)程A,B始終等待時(shí)鐘信號(hào),協(xié)程C執(zhí)行 由于協(xié)程A,B始終等待時(shí)鐘信號(hào),協(xié)程C執(zhí)行 由于協(xié)程A,B始終等待時(shí)鐘信號(hào),協(xié)程C執(zhí)行 協(xié)程A重新獲得執(zhí)行權(quán),并執(zhí)行結(jié)束 協(xié)程B重新獲得執(zhí)行權(quán),并執(zhí)行結(jié)束我們前面提到過(guò),協(xié)程的兩大特點(diǎn),一是可以保存運(yùn)行時(shí)環(huán)境,另一個(gè)便是可以主動(dòng)出讓執(zhí)行權(quán)。那么假如有一個(gè)協(xié)程C始終不出讓執(zhí)行權(quán),即在代碼中,不用await關(guān)鍵字,那么其他協(xié)程是不是就沒(méi)辦法被執(zhí)行了呢,很不幸的是,的確是這樣的。我們看下代碼
import程序運(yùn)行結(jié)果
協(xié)程B開(kāi)始執(zhí)行 協(xié)程B出讓執(zhí)行權(quán) 協(xié)程A開(kāi)始執(zhí)行 協(xié)程A出讓執(zhí)行權(quán) 協(xié)程C不使用await關(guān)鍵字,故不選擇出讓執(zhí)行權(quán),所以繼續(xù)執(zhí)行C 協(xié)程C不使用await關(guān)鍵字,故不選擇出讓執(zhí)行權(quán),所以繼續(xù)執(zhí)行C 協(xié)程C不使用await關(guān)鍵字,故不選擇出讓執(zhí)行權(quán),所以繼續(xù)執(zhí)行C 協(xié)程C不使用await關(guān)鍵字,故不選擇出讓執(zhí)行權(quán),所以繼續(xù)執(zhí)行C 協(xié)程C不使用await關(guān)鍵字,故不選擇出讓執(zhí)行權(quán),所以繼續(xù)執(zhí)行C 協(xié)程C不使用await關(guān)鍵字,故不選擇出讓執(zhí)行權(quán),所以繼續(xù)執(zhí)行C 協(xié)程C不使用await關(guān)鍵字,故不選擇出讓執(zhí)行權(quán),所以繼續(xù)執(zhí)行C 協(xié)程C不使用await關(guān)鍵字,故不選擇出讓執(zhí)行權(quán),所以繼續(xù)執(zhí)行C ...從結(jié)果中我們可以看到,B和A都主動(dòng)出讓了執(zhí)行權(quán),但由于C中雖然同樣調(diào)用了sleep()函數(shù),但是沒(méi)有使用await關(guān)鍵字來(lái)出讓執(zhí)行權(quán),所以始終C就被執(zhí)行,永遠(yuǎn)輪不到A和B執(zhí)行了。
六、總結(jié)
很多講協(xié)程的博客都是從異步/同步的角度出發(fā),但我始終覺(jué)得異步實(shí)際上無(wú)處不在,并不是只有協(xié)程才有的概念,協(xié)程說(shuō)到底就是用戶態(tài)下的線程,如果我們了解清楚線程,包括線程的上下文切換、線程的調(diào)度我們就能很好的理解協(xié)程。
七、后記
終于寫完了這篇博客,為了寫這篇,花了好久的時(shí)間去查資料,還順便把本科的操作系統(tǒng)課的課件翻出來(lái)看了一遍。最大的感受就是想要把這個(gè)內(nèi)容在一篇博客中盡可能的說(shuō)清楚,真的有點(diǎn)難,因?yàn)樯婕暗降膬?nèi)容太多了,上文中還有許多的概念和結(jié)論沒(méi)有展開(kāi)說(shuō),但是限于篇幅,只能日后有需要再進(jìn)行展開(kāi)介紹了。不管怎么說(shuō),這個(gè)flag,算是拔掉了。
總結(jié)
以上是生活随笔為你收集整理的python 协程可以嵌套协程吗_Python线程、协程探究(2)——揭开协程的神秘面纱...的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: python语言格式化输出_Python
- 下一篇: 信号归一化功率_UE低发射功率余量分析