當前位置: 妍妍網 > 碼農

協程、行程、執行緒深入淺出解析分享

2024-05-27碼農

前言

目前我是一名Golang/Python開發工程師,之前是主要使用PHP進行開發的傳統web後端工程師,後面因為工作原因開始接觸並使用Python和Golang來做一些開發工作,涉及到數據分析數倉建設相關及部份遊戲相關的開發;也因為工作原因接觸到了很多其他語言的特性或者是其他語言團體推崇的技術方向方案。

我非常喜歡PHP ,生活中工作中幾乎是能用PHP解決的都盡可能使用PHP,同時也很推崇PHP-cli的開發模式,尤其喜歡 workerman/webman ,早期webman還未誕生的時候我在公司曾使用workerman自建了一套框架,與現在的webman非常相似。

今天我要分享的主要是我所理解的協程相關的分享內容,內容會涉及到行程和執行緒相關的內容點,主要目的是為大家揭開協程神秘的一些面紗,讓大家知道協程並沒有那麽的難,它其實是一種非常簡單易懂的編程方式方案。

在閱讀本分享前,建議先閱讀之前的分享趣談程式演變的過程,有助於理解本分享內容。

阻塞/非阻塞

在文章趣談程式演變的過程中我曾提到兩個概念內容,阻塞與非阻塞;如何理解阻塞與非阻塞呢?很簡單:

阻塞

我去超市買一袋橘子:我需要穿好衣服、下樓、走路、到超市挑選橘子、付款、走路、上樓、到家;在這個流程中,我在買好橘子之前全程被占用,需要做的所有事都需要為買橘子服務,當我做完這一系列事情之後才可以幹下一件事情,比如寫程式碼,比如拉屎等等。

我們可以發現阻塞是一種非常簡單且占用資源比較少的情況,因為全程只需要占用 一個人,只是需要花費的時間比較長。

非阻塞

我透過外賣來實作去超市買一袋橘子:我只需要通知外賣員告知他去xx超市幫我買橘子僅此而已,然後等待外賣員將橘子送達我住處即可;在這個流程中,我在買好橘子之前全程沒有被占用,我隨時都可以幹任何事情,比如吃飯、寫程式碼、拉屎,隨意做我想要做的事情。
但是這裏其實要註意,為了實作這種情況,我們必須要有一個外賣員來幫我們實作,同時我們在告知外賣員的這個過程中其實是被阻塞的,我們在和外賣員溝通的這個過程中。是沒辦法做其他事情的。

我們由此可以發現,非阻塞在某一個微觀內其實多多少少包含了阻塞的部份存在的,並且整體是透過消耗更多的資源(人),從而減少時間的消耗。

行程/執行緒

上面講了非阻塞,講了資源,那麽資源在系統裏面如何實作如何使用呢?答案就是行程或者執行緒(關於行程和執行緒的概念我這裏就不多贅述了,百度都有,可以自行百度)。

PHP中通常來說不使用多執行緒進行編程,通常來說都是使用多行程來實作一些並行效果的,比如workerman/webman就是用了fork來進行多行程的處理,透過不同的onWorkerStart的業務邏輯來實作不同的業務行程,每種業務行程都可以有自己的單/多行程處理方案。

協程

我們現在常談的 協程 ,實際上嚴格意義上來說叫 協程方案 ,它包含了三樣東西在其中:

  1. 協程

  2. 協程排程

  3. 協程執行

協程在一些語言實作中或者在一些文章中又叫纖程,PHP中的fiber、yield分別是有棧協程和無棧協程(關於這個概念可以自行百度,不影響本次分享的內容理解);協程本身不具備並行並列能力,它只是一種程式碼執行的方案,它僅僅需要實作中斷、喚醒即可,這也是fiber、yield的基本功能。

這麽看來,好像協程並沒有什麽用對吧,它好像只能暫停/繼續,為什麽我們非要實作這樣的功能呢?

如果這麽想,其實我們就陷入了 阻塞模式 的思維方式,我們盲目的把寫的程式碼跟獨自買橘子聯系在了一起,從上往下讀,在哪暫停就在那繼續;看似沒用,但假設我們用 非阻塞模式 去思考它,把我們整體的行為按照更小的顆粒度拆分成不同的小行為,交給不同的「外賣員」執行,那麽這樣,是不是就不一樣了?

由於外賣員數量可能存在限制,不可能是無限多個,那麽外賣員就需要根據具體情況具體分析,暫時放下手中的小行為,換另一個小行為先行,同時外賣員也可能找其他外賣員代工,在這個過程中,如果合理利用時間差,那麽每個人都不需要花費太多的時間,合理利用了已存在這世界上的其他外賣員。

對,這就是協程排程器要做的事情,透過一些合理的規劃來進行排程,而外賣員就是協程執行單元!

這裏有許許多多的排程規則,根據不同語言或者不同方案有各自的實作方式,這裏可以自行百度 時間分片 ,有助於理解排程規則。

至於外賣員,是多執行緒/單執行緒實作還是多行程實作都可以。

延申問題

上述內容,其實我們可能會延申出來一些疑惑:

如果不同的小行為之間需要存在關聯關系,外賣員又可能存在找其他外賣員代工的情況,怎麽解決呢?

限制一些特殊情況的小行為不允許代工行為,在Golang中systemcall和netpoll的處理情況就各不同,systemcall不會存在跨執行緒執行,它分配在A執行緒上執行就不會被其他執行緒接管,而netpoll就可能會被其他執行緒接管。

透過上下文的包裹和限制,類似實作一個票據,交到外賣員手中,外賣員每次被排程執行之時都會看一下票據來根據優先順序執行。

那假設我通知完外賣員,我睡著了怎麽辦?我壓根沒有接到外賣員回執怎麽辦?這種情況存在於主執行緒比協程執行單元更先執行完。

我每通知一個外賣員我就在本子上記下一筆,當我自己做完了自己的事情以後,我在房間裏來回踱步,等待外賣員們的回執,回執一個我就劃掉一筆。直到劃完全部,然後再去睡覺;這就是waitGroup,簡單地可以透過一個迴圈來查詢某個計數器判斷跳出實作。

PHP如何實作協程方案

PHP常使用多行程,行程間通訊傳遞資訊極為不方便,同時消耗資源會更多,通常來說不會用行程來實作協程的執行單元;但是我們想到異步想到非阻塞,我們就會想到 event-loop ,對,我們可以透過 event-loop 來實作協程執行單元,將協程執行單元註冊在event-loop中來進行執行。

但是要註意的是實作完整的協程方案除了協程、協程執行單元外還需要一個協程排程器,所以在每個event-loop註冊執行前後需要實作排程器和排程規則才可以,讓event-loop進行合理的回呼的中斷和繼續,合理利用時間。

這樣做其實會讓 event-loop 變得比較臃腫和復雜,不是特別利於維護,整體思路其實和 golang systemcall 實作方案是相似的,因為都是在同一個執行緒上進行執行,不存線上程的切換。

簡單理解

最後呢,其實你把協程方案想象成是一個縮小的佇列系統,由一個程式A釋出訊息(協程),由一個程式B排程(佇列服務),再有另一個程式C進行消費,並在消費後通知來源程式,只不過ABC都是在一個執行緒或者一個程式內執行罷了,它們只是瘋狂利用了時間的空隙罷了。