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