進程 線程 協程 同步 非同步 阻塞 非阻塞

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

參考文章:Python 中的進程、線程、協程、同步、非同步、回調
進程和線程究竟是什麼東西?傳統網路服務模型是如何工作的?協程和線程的關係和區別有哪些?IO過程在什麼時間發生?

一、環境切換

上下文,指的是程式執行的某一個狀態,通常我們會通過調用棧來表示這個狀態,棧記載了每個調用層級執行到了哪裡,以及執行時的環境情況等所有有關的資訊

環境切換,表達的是從一個環境切換到另一個內容相關的技術。“調度”指的是哪個上下文可以獲得接下來CPU時間的方法。

進程

進程是一種古老而經典的上下文系統,每個進程有獨立的地址空間,資源控制代碼,他們之間互不發生幹擾。

每個進程在核心中會有一個資料結構進行描述,我們稱其為進程描述符。這些描述符包含了系統管理進程所需的資訊,並且放在一個叫做任務隊列的隊列裡面。

很顯然,當建立進程時,我們需要分配新的進程描述符,並且分配新的地址空間(和父地址空間的映射保持一致,但是兩者同時進入COW狀態)。這些過程需要一定的開銷。

進程狀態

忽略去linux核心複雜的狀態跳躍表,我們實際上可以把進程狀態歸結為三個最主要的狀態:就緒態,運行態,睡眠態。這就是任何一本系統書上都有的三態轉換圖。

就緒和執行可以互相轉換,基本這就是調度的過程。而當執行態程式需要等待某些條件(最典型就是IO)時,就會陷入睡眠態。而條件達成後,一般會自動進入就緒。

阻塞

當進程需要在某個檔案控制代碼上做IO,這個fd又沒有資料給他的時候,就會發生阻塞。具體來說,就是記錄XX進程阻塞在了XX fd上,然後將進程標記為睡眠態,並調度出去。當fd上有資料時(例如對端發送的資料到達),就會喚醒阻塞在fd上的進程。進程會隨後進入就緒隊列,等待合適的時間被調度。

阻塞後的喚醒也是一個很有意思的話題。當多個上下文阻塞在一個fd上(雖然不多見,但是後面可以看到一個例子),而且fd就緒時,應該喚醒多少個上下文呢?傳統上應當喚醒所有上下文,因為如果僅喚醒一個,而這個上下文又不能消費所有資料時,就會使得其他上下文處於無謂的死結中。

線程

線程是一種輕量進程,實際上在linux核心中,兩者幾乎沒有差別,除了一點——線程並不產生新的地址空間和資源描述符表,而是複用父進程的。 但是無論如何,線程的調度和進程一樣,必須陷入核心態。

二、傳統的網路模型

進程模型

為每個客戶分配一個進程。優點是業務隔離,在一個進程中出現的錯誤不至於影響整個系統,甚至其他進程。Oracle傳統上就是進程模型。缺點是進程的分配和釋放有非常高的成本。因此Oracle需要串連池來保持串連減少建立和釋放,同時盡量複用串連而不是隨意的建立串連。

執行緒模式

為每客戶分配一個線程。優點是更輕量,建立和釋放速度更快,而且多個上下文間的通訊速度非常快。缺點是一個線程出現問題容易將整個系統搞崩潰。

三、C10K問題

進程模型的問題

在C10K的時候,啟動和關閉這麼多進程是不可接受的開銷。事實上單純的進程fork模型在C1K時就應當拋棄了。

Apache的prefork模型,是使用預先分配(pre)的進程池。這些進程是被複用的。但即便是複用,本文所描述的很多問題仍不可避免。

執行緒模式的問題

從任何測試都可以表明,線程模式比進程模式更耐久一些,效能更好。但是在面對C10K還是力不從心的。執行緒模式的問題在於切換成本高。

熟悉linux核心的應該知道,近代linux調度器經過幾個階段的發展。

  • linux2.4的調度器
  • O(1)調度器
  • CFS

實際上直到O(1),調度器的調度複雜度才和隊列長度無關。在此之前,過多的線程會使得開銷隨著線程數增長(不保證線性)。

O(1)調度器看起來似乎是完全不隨著線程的影響。但是這個調度器有顯著的缺點——難於理解和維護,並且在一些情況下會導致互動式程式響應緩慢。 CFS使用紅/黑樹狀結構管理就緒隊列。每次調度,上下文狀態轉換,都會查詢或者變更紅/黑樹狀結構。紅/黑樹狀結構的開銷大約是O(logm),其中m大約為活躍上下文數(準確的說是同優先順序上下文數),大約和活躍的客戶數相當。

因此,每當線程試圖讀寫網路,並遇到阻塞時,都會發生O(logm)層級的開銷。而且每次收到報文,喚醒阻塞在fd上的上下文時,同樣要付出O(logm)層級的開銷。

分析

O(logm)的開銷看似並不大,但是卻是一個無法接受的開銷。因為IO阻塞是一個經常發生的事情。每次IO阻塞,都會發生開銷。而且決定活躍線程數的是使用者,這不是我們可控制的。更糟糕的是,當效能下降,響應速度下降時。同樣的使用者數下,活躍上下文會上升(因為響應變慢了)。這會進一步拉低效能。

問題的關鍵在於,http服務並不需要對每個使用者完全公平,偶爾某個使用者的回應時間大大的延長了是可以接受的。在這種情況下,使用紅/黑樹狀結構去組織待處理fd列表(其實是上下文列表),並且反覆計算和調度,是無謂的開銷

四、多工

簡述

要突破C10K問題,必須減少系統內活躍上下文數(其實未必,例如換一個調度器,例如使用RT的SCHED_RR),因此就要求一個上下文同時處理多個連結。而要做到這點,就必須在每次系統調用讀取或寫入資料時立刻返回。否則上下文持續阻塞在調用上,如何能夠複用?這要求fd處於非阻塞狀態,或者資料就緒。

上文所說的所有IO操作,其實都特指了他的阻塞版本。所謂阻塞,就是上下文在IO調用上等待直到有合適的資料為止。這種模式給人一種“只要讀取資料就必定能讀到”的感覺。而非阻塞調用,就是上下文立刻返回。如果有資料,帶回資料。如果沒有資料,帶回錯誤(EAGAIN)。因此,“雖然發生錯誤,但是不代表出錯”。

但是即使有了非阻塞模式,依然繞不過就緒通知問題。如果沒有合適的就緒通知技術,我們只能在多個fd中盲目的重試,直到碰巧讀到一個就緒的fd為止。這個效率之差可想而知。

在就緒通知技術上,有兩種大的模式——就緒事件通知和非同步IO。其差別簡要來說有兩點

  • 就緒通知維護一個狀態,由使用者讀取,而非同步IO由系統調用使用者的回呼函數
  • 就緒通知在資料就緒時就生效,而非同步IO直到資料IO完成才發生回調

linux下的主流方案一直是就緒通知,其核心態非同步IO方案甚至沒有被封裝到glibc裡去。圍繞就緒通知,linux總共提出過三種解決方案(select、poll、epoll)。我們繞過select和poll方案,看看epoll方案的特性。

另外提一點。有趣的是,當使用了epoll後(更準確說只有在LT模式下),fd是否為非阻塞其實已經不重要了。因為epoll保證每次去讀取的時候都能讀到資料,因此不會阻塞在調用上。

epoll

使用者可以建立一個epoll檔案控制代碼,並且將其他fd和這個"epoll fd"關聯。此後可以通過epoll fd讀取到所有就緒的檔案控制代碼。

epoll有兩大模式,ET和LT。LT模式下,每次讀取就緒控制代碼都會讀取出完整的就緒控制代碼。而ET模式下,只給出上次到這次調用間新就緒的控制代碼。換個說法,如果ET模式下某次讀取出了一個控制代碼,這個控制代碼從未被讀取完過——也就是從沒有從就緒變為未就緒。那麼這個控制代碼就永遠不會被新的調用返回,哪怕上面其實充滿了資料——因為控制代碼無法經曆從非就緒變為就緒的過程。

類似CFS,epoll也使用了紅/黑樹狀結構——不過是用於組織加入epoll的所有fd。epoll的就緒列表使用的是雙向隊列。這方便系統將某個fd排入佇列中,或者從隊列中解除。

要進一步瞭解epoll的具體實現,可以參考這篇linux下poll和epoll核心源碼剖析。

效能

如果使用非阻塞函數,就不存在阻塞IO導致環境切換了,而是變為時間片耗盡被搶佔(大部分情況下如此),因此讀寫的額外開銷被消除。而epoll的常規操作,都是O(1)量級的。而epoll wait的複製動作,則和當前需要返回的fd數有關(在LT模式下幾乎就等同於上面的m,而ET模式下則會大大減少)。

但是epoll存在一點細節問題。epoll fd的管理使用紅/黑樹狀結構,因此在加入和刪除時需要O(logn)複雜度(n為總串連數),而且關聯操作還必須每個fd調用一次。因此在大串連量下頻繁建立和關閉串連仍然有一定效能問題(超短串連)。不過關聯操作調用畢竟比較少。如果確實是超短串連,tcp串連和釋放開銷就很難接受了,所以對總體效能影響不大。

固有缺陷

原理上說,epoll實現了一個wait_queue的回呼函數,因此原理上可以監聽任何能夠啟用wait_queue的對象。但是epoll的最大問題是無法用於普通檔案,因為普通檔案始終是就緒的——雖然在讀取的時候不是這樣。

這導致基於epoll的各種方案,一旦讀到普通檔案上下文仍然會阻塞。golang為瞭解決這個問題,在每次調用syscall的時候,會獨立的啟動一個線程,在獨立的線程中進行調用。因此golang在IO普通檔案的時候網路不會阻塞。

五、事件通知機制下的幾種程式設計模型

簡述

使用通知機制的一大缺憾就是,使用者進行IO操作後會陷入茫然——IO沒有完成,所以當前上下文不能繼續執行。但是由於複用線程的要求,當前線程還需要接著執行。所以,在如何進行非同步編程上,又分化出數種方案。

使用者態調度

首先需要知道的一點就是,非同步編程大多數情況下都伴隨著使用者態調度問題——即使不使用上下文技術。

因為系統不會自動根據fd的阻塞狀況來喚醒合適的上下文了,所以這個工作必須由其他人——一般就是某種架構——來完成。

你可以想像一個fd映射到對象的大map表,當我們從epoll中得知某個fd就緒後,需要喚醒某種對象,讓他處理fd對應的資料。

當然,實際情況會更加複雜一些。原則上所有不佔用CPU時間的等待都需要中斷執行,陷入睡眠,並且交由某種機構管理,等待合適的機會被喚醒。例如sleep,或是檔案IO,還有lock。更精確的說,所有在核心裡面涉及到wait_queue的,在架構裡面都需要做這種機制——也就是把核心的調度和等待搬到使用者態來。

當然,其實也有反過來的方案——就是把程式扔到核心裡面去。其中最著名的執行個體大概是微軟的http伺服器了。

這個所謂的“可喚醒可中斷對象”,用的最多的就是協程。

協程

協程是一種編程組件,可以在不陷入核心的情況進行環境切換。如此一來,我們就可以把協程內容物件關聯到fd,讓fd就緒後協程恢複執行。 當然,由於當前地址空間和資源描述符的切換無論如何需要核心完成,因此協程所能調度的,只有在同一進程中的不同上下文而已。

如何做到

這是如何做到的呢?

我們在核心裡實行環境切換的時候,其實是將當前所有寄存器儲存到記憶體中,然後從另一塊記憶體中載入另一組已經被儲存的寄存器。對於圖靈機來說,目前狀態寄存器意味著機器狀態——也就是整個上下文。其餘內容,包括棧上記憶體,堆上對象,都是直接或者間接的通過寄存器來訪問的。

但是請仔細想想,寄存器更換這種事情,似乎不需要進入核心態麼。事實上我們在使用者態切換的時候,就是用了類似方案。

C coroutine的實現,基本大多是儲存現場和恢複之類的過程。python則是儲存當前thread的top frame(greenlet)。

但是非常悲劇的,純使用者態方案(setjmp/longjmp)在多數系統上執行的效率很高,但是並不是為了協程而設計的。setjmp並沒有拷貝整個棧(大多數的coroutine方案也不應該這麼做),而是只儲存了寄存器狀態。這導致新的寄存器狀態和老寄存器狀態共用了同一個棧,從而在執行時互相破壞。而完整的coroutine方案應當在特定時刻建立一個棧。

而比較好的方案(makecontext/swapcontext)則需要進入核心(sigprocmask),這導致整個調用的效能非常低。

協程與線程的關係

首先我們可以明確,協程不能調度其他進程中的上下文。而後,每個協程要獲得CPU,都必須線上程中執行。因此,協程所能利用的CPU數量,和用於處理協程的線程數量直接相關。

作為推論,在單個線程中執行的協程,可以視為單線程應用。這些協程,在未執行到特定位置(基本就是阻塞操作)前,是不會被搶佔,也不會和其他CPU上的上下文發生同步問題的。因此,一段協程代碼,中間沒有可能導致阻塞的調用,執行在單個線程中。那麼這段內容可以被視為同步的。

我們經常可以看到某些協程應用,一啟動就是數個進程。這並不是跨進程調度協程。一般來說,這是將一大群fd分給多個進程,每個進程自己再做fd-協程對應調度。

基於就緒通知的協程架構

首先需要封裝read/write,在發生read的時候檢查返回。如果是EAGAIN,那麼將當前協程標記為阻塞在對應fd上,然後執行調度函數。
調度函數需要執行epoll(或者從上次的返回結果緩衝中取資料,減少核心陷入次數),從中讀取一個就緒的fd。如果沒有,上下文應當被阻塞到至少有一個fd就緒。
尋找這個fd對應的協程內容物件,並調度過去。
當某個協程被調度到時,他多半應當在調度器返回的路上——也就是read/write讀不到資料的時候。因此應當再重試讀取,失敗的話返回1。
如果讀取到資料了,直接返回。
這樣,非同步資料讀寫動作,在我們的想像中就可以變為同步的。而我們知道同步模型會極大降低我們的編程負擔。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.