亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频

蟲蟲首頁| 資源下載| 資源專輯| 精品軟件
登錄| 注冊

您現在的位置是:首頁 > 技術閱讀 >  圖解|工作6年多,我還是沒有搞懂什么是協程的道與術

圖解|工作6年多,我還是沒有搞懂什么是協程的道與術

時間:2024-02-11

前言

大家好,我的朋友們!

大白干了6年多后端,寫過C/C++、Python、Go,每次說到協程的時候,腦海里就只能浮現一些關鍵字yeild、async、go等等。

但是對于協程這個知識點,我理解的一直比較模糊,于是決定搞清楚。

全文閱讀預計耗時10分鐘,少刷幾個小視頻的時間,多學點知識,想想就很劃算噻!

協程概念的誕生

先拋一個粗淺的結論:協程從廣義來說是一種設計理念,我們常說的只是具體的實現

理解好思想,技術點就很簡單了,關于協程道與術的區別:

上古神器COBOL

協程概念的出現比線程更早,甚至可以追溯到20世紀50年代,提協程就必須要說到一門生命力極強的最早的高級編程語言COBOL。

最開始我以為COBOL這門語言早就消失在歷史長河中,但是我錯了。

COBOL語言,是一種面向過程的高級程序設計語言,主要用于數據處理,是國際上應用最廣泛的一種高級語言。COBOL是英文Common Business-Oriented Language的縮寫,原意是面向商業的通用語言。

截止到今年在全球范圍內大約有1w臺大型機中有3.8w+遺留系統中約2000億行代碼是由COBOL寫的,占比高達65%,同時在美國很多政府和企業機構都是基于COBOL打造的,影響力巨大。

時間拉回1958年,美國計算機科學家梅爾文·康威(Melvin Conway)就開始鉆研基于磁帶存儲的COBOL的編譯器優化問題,這在當時是個非常熱門的話題,不少青年才俊都撲進去了,包括圖靈獎得主唐納德·爾文·克努斯教授(Donald Ervin Knuth)也寫了一個優化后的編譯器。

看看這兩位的簡介,我沉默了:

梅爾文·康威(Melvin Conway)也是一位超級大佬,著名的康威定律提出者。

唐納德·爾文·克努斯是算法和程序設計技術的先驅者,1974年的圖靈獎得主,計算機排版系統TeX和字型設計系統METAFONT的發明者,他因這些成就和大量創造性的影響深遠的著作而譽滿全球,《計算機程序設計的藝術》被《美國科學家》雜志列為20世紀最重要的12本物理科學類專著之一。

那究竟是什么問題讓這群天才們投入這么大的精力呢?快來看看!

COBOL編譯器的技術難題

我們都是知道高級編程語言需要借助編譯器來生成二進制可執行文件,編譯器的基本步驟包括:讀取字符流、詞法分析、語法分析、語義分析、代碼生成器、代碼優化器等

這種管道式的流程,上一步的輸出作為下一步的輸入,將中間結果存儲在內存即可,這在現代計算機上毫無壓力,但是受限于軟硬件水平,在幾十年前的COBOL語言卻是很難的。

在1958年的時候,當時的存儲還不發達,磁帶作為存儲器是1951年在計算機中得到應用的,所以那個時代的COBOL很依賴于磁帶。

其實,我在網上找了很多資料去看當時的編譯器有什么問題,只找到了一條:編譯器無法做到讀一次磁帶就可以完成整個編譯過程,也就是所謂的one-pass編譯器還沒有產生。

當時的COBOL程序被寫在一個磁帶上,而磁帶不支持隨機讀寫,只能順序讀,而當時的內存又不可能把整個磁帶的內容都裝進去,所以一次讀取沒編譯完就要再從頭讀。

于是,我腦補了COBOL編譯器和磁帶之間可能的兩種multi-pass形式的交互情況:

  • 可能情況一
    對于COBOL的編譯器來說,要完成詞法分析、語法分析就要從磁帶上讀取程序的源代碼,在之前的編譯器中詞法分析和語法分析是相互獨立的,這就意味著:

    • 詞法分析時需要將磁帶從頭到尾過一遍
    • 語法分析時需要將磁帶從頭到尾過一遍
  • 可能情況二
    聽過磁帶的朋友們一定知道磁帶的兩個基本操作:倒帶和快進。
    在完成編譯器的詞法分析和語法分析兩件事情時,需要磁帶反復的倒帶和快進去尋找兩類分析所需的部分,類似于磁盤的尋道,磁頭需要反復移動橫跳,并且當時的磁帶不一定支持隨機讀寫。

從一些資料可以看到,COBOL當時編譯器各個環節相互獨立的,這種軟硬件的綜合限制導致無法實現one-pass編譯。

協同式解決方案

在梅爾文·康威的編譯器設計中將詞法分析和語法分析合作運行,而不再像其他編譯器那樣相互獨立,兩個模塊交織運行,編譯器的控制流在詞法分析和語法分析之間來回切換

  • 當詞法分析模塊基于詞素產生足夠多的詞法單元Token時就控制流轉給語法分析
  • 當語法分析模塊處理完所有的詞法單元Token時將控制流轉給詞法分析模塊
  • 詞法分析和語法分析各自維護自身的運行狀態,并且具備主動讓出和恢復的能力

可以看到這個方案的核心思想在于:

梅爾文·康威構建的這種協同工作機制,需要參與者讓出(yield)控制流時,記住自身狀態,以便在控制流返回時能從上次讓出的位置恢復(resume)執行。簡言之,協程的全部精神就在于控制流的主動讓出和恢復

這種協作式的任務流和計算機中斷非常像,在當時條件的限制下,由梅爾文·康威提出的這種讓出/恢復模式的協作程序被認為是最早的協程概念,并且基于這種思想可以打造新的COBOL編譯器。

在1963年,梅爾文·康威也發表了一篇論文來說明自己的這種思想,雖然半個多世紀過去了,有幸我還是找到了這篇論文:

https://melconway.com/Home/pdf/compiler.pdf

說實話這paper真是有點難,時間過于久遠,很難有共鳴,最后我放棄了,要不然我或許能搞明白之前編譯器的具體問題了。

懷才不遇的協程

雖然協程概念出現的時間比線程還要早,但是協程一直都沒有正是登上舞臺,真是有點懷才不遇的趕腳。

我們上學的時候,老師就講過一些軟件設計思想,其中主流語言崇尚自頂向下top-down的編程思想:

對要完成的任務進行分解,先對最高層次中的問題進行定義、設計、編程和測試,而將其中未解決的問題作為一個子任務放到下一層次中去解決。

這樣逐層、逐個地進行定義、設計、編程和測試,直到所有層次上的問題均由實用程序來解決,就能設計出具有層次結構的程序。

C語言就是典型的top-down思想的代表,在main函數作為入口,各個模塊依次形成層次化的調用關系,同時各個模塊還有下級的子模塊,同樣有層次調用關系。

但是協程這種相互協作調度的思想和top-down是不合的,在協程中各個模塊之間存在很大的耦合關系,并不符合高內聚低耦合的編程思想,相比之下top-down使程序結構清晰、層次調度明確,代碼可讀性和維護性都很不錯。

與線程相比,協作式任務系統讓調用者自己來決定什么時候讓出,比操作系統的搶占式調度所需要的時間代價要小很多,后者為了能恢復現場會在切換線程時保存相當多的狀態,并且會非常頻繁地進行切換,資源消耗更大。

綜合來說,協程完全是用戶態的行為,由程序員自己決定什么時候讓出控制權,保存現場和切換恢復使用的資源也非常少,同時對提高處理器效率來說也是完全符合的

那么不禁要問:協程看著不錯,為啥沒成為主流呢?

  • 協程的思想和當時的主流不符合
  • 搶占式的線程可以解決大部分的問題,讓使用者感受的痛點不足

換句話說:協程能干的線程干得也不錯,線程干的不好的地方,使用者暫時也不太需要,所以協程就這樣懷才不遇了。

其實,協程雖然在x86架構上沒有折騰出大風浪,由于搶占式任務系統依賴于CPU硬件的支持,對硬件要求比較高,對于一些嵌入式設備來說,協同調度再合適不過了,所以協程在另外一個領域也施展了拳腳。

協程的雄起

我們對于CPU的壓榨從未停止。

對于CPU來說,任務分為兩大類:計算密集型和IO密集型

計算密集型已經可以最大程度發揮CPU的作用,但是IO密集型一直是提高CPU利用率的難點。

IO密集型任務之痛

對于IO密集型任務,在搶占式調度中也有對應的解決方案:異步+回調

也就是遇到IO阻塞,比如下載圖片時會立即返回,等待下載完成將結果進行回調處理,交付給發起者。

就像你常去早餐店,油條還沒好,你和老板很熟悉就先交了錢去座位玩手機了,等你的油條好了,服務員就端過去了,這就是典型的異步+回調。

雖然異步+回調在現實生活中看著也很簡單,但是在程序設計上卻很讓人頭痛,在某些場景下會讓整個程序的可讀性非常差,而且也不好寫,相反同步IO雖然效率低,但是很好寫,

還是以為異步圖片下載為例,圖片服務中臺提供了異步接口,發起者請求之后立即返回,圖片服務此時給了發起者一個唯一標識ID,等圖片服務完成下載后把結果放到一個消息隊列,此時需要發起者不斷消費這個MQ才能拿到下載結果。

整個過程相比同步IO來說,原來整體的邏輯被拆分為好幾個部分,各個子部分有狀態的遷移,對大部分程序員來說維護狀態簡直就是噩夢,日后必然是bug的高發地

用戶態協同調度

隨著網絡技術的發展和高并發要求,對于搶占式調度對IO型任務處理的低效逐漸受到重視,終于協程的機會來了。

協程將IO的處理權交給了程序員,遇到IO被阻塞時就交出控制權給其他協程,等其他協程處理完再把控制權交回來。

通過yield方式轉移執行權的多個協程之間并非調用者和被調用者的關系,而是彼此平等、對稱、合作的關系。

協程一直沒有占上風的原因,除了設計思想的矛盾,還有一些其他原因,畢竟協程也不是銀彈,來看看協程有什么問題:

  • 協程無法利用多核,需要配合進程來使用才可以在多CPU上發揮作用
  • 線程的回調機制仍然有巨大生命力,協程無法全部替代
  • 控制權需要轉移可能造成某些協程的饑餓,搶占式更加公平
  • 協程的控制權由用戶態決定可能轉移給某些惡意的代碼,搶占式由操作系統來調度更加安全

綜上來說,協程和線程并非矛盾,協程的威力在于IO的處理,恰好這部分是線程的軟肋,由對立轉換為合作才能開辟新局面

擁抱協程的編程語言

網絡操作、文件操作、數據庫操作、消息隊列操作等重IO操作,是任何高級編程語言無法避開的問題,也是提高程序效率的關鍵。

像Java、C/C++、Python這些老牌語言也陸續開始借助于第三方包來支持協程,來解決自身語言的不足。

像Golang這種新生選手,在語言層面原生支持了協程,可以說是徹底擁抱協程,這也造就了Go的高并發能力。

我們來分別看看它們是怎么實現協程的,以及實現協程的關鍵點是什么。

Python

Python對協程的支持也經歷了多個版本,從部分支持到完善支持一直在演進:

  • Python2.x對協程的支持比較有限,生成器yield實現了一部分但不完全
  • 第三方庫gevent對協程的實現有比較好,但不是官方的
  • Python3.4加入了asyncio模塊
  • 在Python3.5中又提供了async/await語法層面的支持
  • Python3.6中asyncio模塊更加完善和穩
  • Python3.7開始async/await成為保留關鍵字

我們以最新的async/await來說明Python的協程是如何使用的:

import asyncio
from pathlib import Path
import logging
from urllib.request import urlopen, Request
import os
from time import time
import aiohttp
 
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
 
 
CODEFLEX_IMAGES_URLS = ['https://codeflex.co/wp-content/uploads/2021/01/pandas-dataframe-python-1024x512.png',
                        'https://codeflex.co/wp-content/uploads/2021/02/github-actions-deployment-to-eks-with-kustomize-1024x536.jpg',
                        'https://codeflex.co/wp-content/uploads/2021/02/boto3-s3-multipart-upload-1024x536.jpg',
                        'https://codeflex.co/wp-content/uploads/2018/02/kafka-cluster-architecture.jpg',
                        'https://codeflex.co/wp-content/uploads/2016/09/redis-cluster-topology.png']
 
 
async def download_image_async(session, dir, img_url):
    download_path = dir / os.path.basename(img_url)
    async with session.get(img_url) as response:
        with download_path.open('wb') as f:
            while True:
                chunk = await response.content.read(512)
                if not chunk:
                    break
                f.write(chunk)
    logger.info('Downloaded: ' + img_url)
 
 
async def main():
    images_dir = Path("codeflex_images")
    Path("codeflex_images").mkdir(parents=False, exist_ok=True)
 
    async with aiohttp.ClientSession() as session:
        tasks = [(download_image_async(session, images_dir, img_url)) for img_url in CODEFLEX_IMAGES_URLS]
        await asyncio.gather(*tasks, return_exceptions=True)
 
 
if __name__ == '__main__':
    start = time()
     
    event_loop = asyncio.get_event_loop()
    try:
        event_loop.run_until_complete(main())
    finally:
        event_loop.close()
 
    logger.info('Download time: %s seconds', time() - start)

這段代碼展示了如何使用async/await來實現圖片的并發下載功能。

  • 在普通的函數def前面加async關鍵字就變成異步/協程函數,調用該函數并不會運行,而是返回一個協程對象,后續在event_loop中執行
  • await表示等待task執行完成,也就是yeild讓出控制權,同時asyncio使用事件循環event_loop來實現整個過程,await需要在async標注的函數中使用
  • event_loop事件循環充當管理者的角色,將控制權在幾個協程函數之間切換

C++

在C++20引入協程框架,但是很不成熟,換句話說是給寫協程庫的大佬用的最底層的東西,用起來就很復雜門檻比較高。

C++作為高性能服務器開發語言的無冕之王,各大公司也做了很多嘗試來使用協程功能,比如boost.coroutine、微信的libco、libgo、云風用C實現的協程庫等。

說實話,C++協程相關的東西有點復雜,后面專門寫一下,在此不展開了。

Go

go中的協程被稱為goroutine,被認為是用戶態更輕量級的線程,協程對操作系統而言是透明的,也就是操作系統無法直接調度協程,因此必須有個中間層來接管goroutine。

goroutine仍然是基于線程來實現的,因為線程才是CPU調度的基本單位,在go語言內部維護了一組數據結構和N個線程,協程的代碼被放進隊列中來由線程來實現調度執行,這就是著名的GMP模型。

  • G:Goroutine

每個Gotoutine對應一個G結構體,G存儲Goroutine的運行堆棧,狀態,以及任務函數,可重用函數實體G需要保存到P的隊列或者全局隊列才能被調度執行。

  • M:machine

M是線程的抽象,代表真正執行計算的資源,在綁定有效的P后,進入調度執行循環,M會從P的本地隊列來執行,

  • P:Processor

P是一個抽象的概念,不是物理上的CPU而是表示邏輯處理器。當一個P有任務,需要創建或者喚醒一個系統線程M去處理它隊列中的任務。

P決定同時執行的任務的數量,GOMAXPROCS限制系統線程執行用戶層面的任務的數量。

對M來說,P提供了相關的執行環境,入內存分配狀態,任務隊列等。

GMP模型運行的基本過程

  • 首先創建一個G對象,然后G被保存在P的本地隊列或者全局隊列
  • 這時P會喚醒一個M,M尋找一個空閑的P將G移動到它自己,然后M執行一個調度循環:調用G對象->執行->清理線程->繼續尋找Goroutine。
  • 在M的執行過程中,上下文切換隨時發生。當切換發生,任務的執行現場需要被保護,這樣在下一次調度執行可以進行現場恢復。
  • M的棧保存在G對象,只有現場恢復需要的寄存器(SP,PC等),需要被保存到G對象。

總結

本文通過1960年對COBOL語言編譯器的one-pass問題的介紹,讓大家看到了協同式程序的最早背景以及主動讓出/恢復的重要理念。

緊接著介紹了主流的自頂向下的軟件設計思想和協程思想的矛盾所在,并且搶占式程序調度的蓬勃發展,以及存在的問題。

繼續介紹了關于IO密集型任務對于提升CPU效率的阻礙,搶占式調度對于IO密集型問題的異步+回調的解決方案,以及協程的處理,展示了協程在IO密集型任務上處理的重大優勢。

最后說明了當前搶占式調度+協程IO密集型處理的方案,包括Python、C++和go的語言層面對于協程的支持和實現。

本文特別具體的內容并不多,旨在介紹協程思想及其優勢所在,對于各個語言的協程實現細節并未展開。



往期推薦



參加了 40 多場面試。

如何調試內存泄漏?方法論來了

清華大學:2021 元宇宙研究報告!

寫了一段“高端”C語言代碼

手擼一個線程池

Linux 中的各種棧:進程棧 線程棧 內核棧 中斷棧

new[]和delete[]一定要配對使用嗎?

系統調用如何實現?

如何閱讀開源項目代碼

C++20新特性的小細節

分享一個編程設計小技巧(沒有兩三年工作經驗估計看不懂)

多線程學習指南

手擼一個對象池

這里收集了100多篇C++原創文章(入門進階必備)

手寫線程池 - C語言版

if-else和switch-case哪個效率更高?看這四張圖。

看完這篇你還能不懂C語言/C++內存管理?

從未見過把內存玩的如此明白的文章(推薦大家都來看看)


主站蜘蛛池模板: 绥芬河市| 安庆市| 泸西县| 临西县| 东乡族自治县| 天峻县| 巢湖市| 宣威市| 南漳县| 虞城县| 内丘县| 周口市| 厦门市| 房产| 隆回县| 沂水县| 华坪县| 攀枝花市| 彰武县| 祥云县| 新民市| 西林县| 青浦区| 木兰县| 南丰县| 祥云县| 印江| 南昌市| 肇东市| 寿阳县| 手游| 遂平县| 华池县| 林州市| 岑溪市| 巨鹿县| 古丈县| 乐亭县| 仙桃市| 汽车| 石河子市|