Callback與coroutine 協程概念說明

小談阻塞非阻塞

阻塞非阻塞概念都是對於線程, 進程這種粒度來講的, 由於只有他們纔是內核有感知的, 協程是你內核無感知, 是你用戶本身實現的.java

例如在 Golang 中, resp, err := client.Do(req) 看着是阻塞的寫法(有網絡 IO), 可是 Go 的 Http 包是異步的, 這邊在協程粒度是阻塞住了, 可是線程該幹嗎就幹嗎去了, 對於系統來講, 就是非阻塞. 應用程序以爲我遇到阻塞, 好比 I/O什麼的, 我就 yield 出去, 把控制權交出去.程序員

分清楚內核層和應用層是關鍵web

阻塞仍是非阻塞 應用程序的調用是否當即返回. 被掛起沒法執行其餘操做的則是阻塞型的, 能夠被當即「抽離」去完成其餘「任務」的則是非阻塞型的. 算法

可是掛起就不能幹事情了嗎, 答案是否認的, 一個線程讀文件,被阻塞了,資源會出讓(陳力就列, 不能者止). coroutine也是, 可是好比 goroutine, go的調度器會把處於阻塞的的go程上的資源分配給其餘go程. 可是這裏的重點就是線程切換的代價比協程切換的代價高不少(線程切換涉及到內核態和用戶態的切換)編程

協程線程 調度一個主動(協做式調度, 應用程序本身調度) 一個被動(搶佔式調度, 操做系統調度)緩存

異步和同步 數據 copy 時進程是否阻塞. 同步:應用層本身去想內核詢問(輪詢?); 異步:內核主動通知應用層數據. IO 操做分爲兩個過程:內核等待事件, 把數據拷貝到用戶緩衝區. 這兩個過程只要等待任何一個都是同步 IO安全

在異步非阻塞模型中, 沒有無謂的掛起、休眠與等待, 也沒有盲目無知的問詢與檢查, 應用層作到不等候片刻的最大化利用自身的資源, 系統內核也十分「善解人意」的在完成任務後主動通知應用層來接收任務成果.服務器

進程、線程、協程

1. 進程

操做系統中最核心的概念是進程, 分佈式系統中最重要的問題是進程間通訊.網絡

進程是具備必定獨立功能的程序關於某個數據集合上的一次運行活動,進程是系統進行資源分配和調度的一個獨立單位. 每一個進程都有本身的獨立內存空間, 不一樣進程經過進程間通訊來通訊. 因爲進程比較重量, 佔據獨立的內存, 因此上下文進程間的切換開銷(棧、寄存器、虛擬內存、文件句柄等)比較大, 但相對比較穩定安全.數據結構

進程是「程序執行的一個實例」 , 擔當分配系統資源的實體. 進程建立必須分配一個完整的獨立地址空間.

進程切換隻發生在內核態, 兩步:1. 切換頁全局目錄以安裝一個新的地址空間 2. 切換內核態堆棧和硬件上下文. 另外一種說法相似:1 保存CPU環境(寄存器值、程序計數器、堆棧指針); 2. 修改內存管理單元MMU的寄存器; 3. 轉換後備緩衝器TLB中的地址轉換緩存內容標記爲無效

事件模型

基於事件的模型, 一個進程處理多個請求, 而且經過epoll機制來通知用戶請求完成, 事件驅動適合於IO密集型服務, 多進程或線程適合於CPU密集型服務

Nginx 在一個工做進程中處理多個鏈接和請求, 由於滿負載進程的數量不多(一般每核CPU只有一個)並且恆定, 因此任務切換隻消耗不多的內存, 並且不會浪費CPU週期, 固然前提是沒有阻塞

爲何不使用不少進程: 每一個進程都消耗額外的內存, 並且每次進程間的切換都會消耗CPU週期並丟棄CPU高速緩存中的數據(全部處理過程是在一個簡單的循環中, 由一個線程完成)

1.1 建立進程的過程

  1. 建立一個PCB
  2. 賦予一個統一進程標識符
  3. 爲進程映象分配空間
  4. 初始化進程控制塊
  5. 許多默認值 (如: 狀態爲 New, 無I/O設備或文件...)
  6. 設置相應的連接. 如: 把新進程加到就緒隊列的鏈表中

1.2 進程切換步驟(PCB/TLB):

  1. 保存被中斷進程的處理器現場信息
  2. 修改被中斷進程的進程控制塊的有關信息, 如進程狀態等
  3. 把被中斷進程的PCB加入有關隊列
  4. 選擇下一個佔有處理器運行的進程
  5. 修改被選中進程的PCB的有關信息
  6. 根據被選中進程設置操做系統用到的地址轉換和存儲保護信息
  7. 根據被選中進程恢復處理器現場

進程上下文:

進程上下文:操做系統中把進程物理實體和支持進程運行的環境合稱爲進程上下文(context). 進程實體+運行環境.

  • 用戶級上下文:由用戶程序塊、用戶數據塊和用戶堆棧組成的進程地址空間.
  • 系統級上下文:由進程控制塊、內存管理信息、進程環境塊, 及系統堆棧等組成的進程地址空間.
  • 寄存器上下文:由PSW寄存器和各種控制寄存器、地址寄存器、通用寄存器組成、用戶棧指針等組成.

進程阻塞過程

中止當前進程的執行;保存該進程的CPU現場信息;將進程狀態改成阻塞態, 並將其PCB入相應的阻塞隊列;轉進程調度程序.

進程喚醒過程

首先把被阻塞的進程從等待該事件的阻塞隊列中移出, 將其PCB中的現行狀態由阻塞改成就緒, 而後再將該PCB插入到就緒隊列中. 進程切換:中斷處於運行態的進程運行, 讓出處理器, 恢復新進程的狀態, 使新進程投入運行. 當系統調度新進程佔有處理器時, 新老進程隨之發生上下文切換. 進程的運行被認爲是在進程的上下文中執行的.

2. 線程

線程是進程的一個實體,是CPU調度和分派的基本單位,它是比進程更小的能獨立運行的基本單位.線程本身基本上不擁有系統資源,只擁有一點在運行中必不可少的資源(如程序計數器,一組寄存器和棧),可是它可與同屬一個進程的其餘的線程共享進程所擁有的所有資源. 線程間通訊主要經過共享內存, 上下文切換很快, 資源開銷較少, 但相比進程不夠穩定容易丟失數據.

一旦建立完線程, 你就沒法決定他何時得到時間片, 何時讓出時間片了, 你把它交給了內核. 由於程序的使用涉及大量的計算機資源配置, 把這活隨意的交給用戶程序, 很是容易讓整個系統分分鐘被搞跪, 資源分配也很難作到相對的公平. 因此核心的操做須要陷入內核(kernel), 切換到操做系統, 讓老大幫你來作.

線程上下文通常只包含CPU上下文及其餘的線程管理信息. 線程建立的開銷主要取決於爲線程堆棧的創建而分配內存的開銷, 這些開銷並不大. 線程上下文切換髮生在兩個線程須要同步的時候, 好比進入共享數據段. 切換隻CPU寄存器值須要存儲, 並隨後用將要切換到的線程的原先存儲的值從新加載到CPU寄存器中去.

有的時候碰着I/O訪問, 阻塞了後面全部的計算. 空着也是空着, 老大就直接把CPU切換到其餘進程, 讓人家先用着. 固然除了I\O阻塞, 還有時鐘阻塞等等.可是一切換進程得反覆進入內核, 置換掉一大堆狀態. 進程數一高, 大部分系統資源就被進程切換給吃掉了. 後來搞出線程的概念, 這個地方阻塞了, 但我還有其餘地方的邏輯流能夠計算, 這些邏輯流是共享一個地址空間的, 不用特別麻煩的切換頁表、刷新TLB, 只要把寄存器刷新一遍就行, 能比切換進程開銷少點.

3. 協程

一個Coroutine若是處於block狀態, 能夠交出執行權, 讓其餘的coroutine繼續執行(這個是由其實現調度的,對用戶透明的).

Coroutines使得開發者能夠採用阻塞式的開發風格,卻可以實現非阻塞I/O的效果隱式事件調度

函數實際上是協程的特例, 從Knuth老爺子的基本算法捲上看「子程序實際上是協程的特例」. 子程序是什麼?子程序(英語:Subroutine, procedure, function, routine, method, subprogram), 就是函數嘛!因此協程也沒什麼了不得的, 就是種更通常意義的程序組件, 那你內存空間夠大, 建立多少個函數還不是隨你麼?

協程能夠經過yield來調用其它協程. 經過yield方式轉移執行權的協程之間不是調用者與被調用者的關係, 而是彼此對稱、平等的. 協程的起始處是第一個入口點, 在協程裏, 返回點以後是接下來的入口點. 子例程的生命期遵循後進先出(最後一個被調用的子例程最早返回);相反, 協程的生命期徹底由他們的使用的須要決定. (continuation)

協程是一種用戶態的輕量級線程, 協程的調度徹底由用戶控制. 協程擁有本身的寄存器上下文和棧. 協程調度切換時, 將寄存器上下文和棧保存到其餘地方, 在切回來的時候, 恢復先前保存的寄存器上下文和棧, 直接操做棧則基本沒有內核切換的開銷, 能夠不加鎖的訪問全局變量, 因此上下文的切換很是快.

協程能夠經過yield來調用其它協程. 經過yield方式轉移執行權的協程之間不是調用者與被調用者的關係, 而是彼此對稱、平等的. 協程的起始處是第一個入口點, 在協程裏, 返回點以後是接下來的入口點. 子例程的生命期遵循後進先出(最後一個被調用的子例程最早返回);相反, 協程的生命期徹底由他們的使用的須要決定.

協程和線程的區別是:協程避免了無心義的調度, 由此能夠提升性能, 但也所以, 程序員必須本身承擔調度的責任, 同時, 協程也失去了標準線程使用多CPU的能力

協程編寫者能夠有一是可控的切換時機, 二是很小的切換代價. 從操做系統有沒有調度權上看, 協程就是由於不須要進行內核態的切換, 因此會使用它

協程好處

  • 無需線程上下文切換的開銷

  • 無需原子操做鎖定及同步的開銷

  • 狀態機:在一個子例程裏實現狀態機, 這裏狀態由該過程當前的出口/入口點肯定;這能夠產生可讀性更高的代碼.

  • 角色模型:並行的角色模型, 例如計算機遊戲. 每一個角色有本身的過程(這又在邏輯上分離了代碼), 但他們自願地向順序執行各角色過程的中央調度器交出控制(這是合做式多任務的一種形式).

  • 產生器:它有助於輸入/輸出和對數據結構的通用遍歷

缺點

  • 不能同時將 CPU 的多個核.不過如今使用協程的語言都用到了多調度器的架構, 單進程下的協程也能用多核了

說了這麼多, 無非是說明, coroutine 是從另外一個方向演化而來, 它是對 continuation 概念的簡化. Lua 設計者反覆提到, coroutine is one-shot semi-continuation.

其餘說明

  1. 歷史上先有協程,OS模擬多任務併發, 非搶佔式
  2. 線程能利用多核達到真正的並行計算, 說線程性能很差是由於設計的很差, 有大量的鎖/切換/等待. 同一時間只有一個協程擁有運行權, 因此至關於單線程的能力
  3. 說協程性能好的,真正緣由是瓶頸在IO上面,這時候發揮不了線程的做用
  4. 事件驅動, Callback也挺不錯
  5. 協程能夠用同步的代碼感受 寫出異步的效果

分割線


進程、線程、協程的關係和區別:

方面: cpu調度, 上下文切換, 數 據共享, 多核cup利用率, 資源佔用角度.

線程進程都是同步機制, 而協程則是異步,協程能保留上一次調用時的狀態, 每次過程重入時, 就至關於進入上一次調用的狀態(continuation)

線程和進程主要區別是在輕量級和重量級, 他們調度都是系統來的; 協程和線程的區別是在調度:協程避免了無心義的調度, 由此能夠提升性能, 但也所以, 程序員必須本身承擔調度的責任, 同時, 協程也失去了標準線程使用多CPU的能力.

  • 進程擁有本身獨立的堆和棧, 既不共享堆, 亦不共享棧, 進程由操做系統調度. 一個進程死亡對其餘進程沒有影響

  • 線程擁有本身獨立的棧和共享的堆, 共享堆, 不共享棧, 線程亦由操做系統調度(標準線程是的), 沒有本身獨立的地址空間!!! 默認狀況下, 線程棧的大小爲1MB. 線程私有棧, 一個線程掛掉將致使整個進程掛掉, 由於線程沒有本身單獨的內存地址空間. 當一個線程向非法地址讀取或者寫入(伴隨棧溢出、讀取或者訪問了非法地址), 沒法確認這個操做是否會影響同一進程中的其它線程, 因此只能是整個進程一塊兒崩潰. (注意 這個崩潰不是 java 的異常哦, jvm 作了不少保護)

  • 協程和線程同樣共享堆, 不共享棧, 協程由程序員在協程的代碼裏顯示調度. 棧內存(大概是4~5KB)

  1. 須要頻繁建立銷燬的優先用線程. 實例:web服務器. 來一個創建一個線程, 斷了就銷燬線程. 要是用進程, 建立和銷燬的代價是很難承受的.

  2. 須要進行大量計算的優先使用線程. 所謂大量計算, 固然就是要消耗不少cpu, 切換頻繁了, 這種狀況先線程是最合適的. 實例:圖像處理、算法處理.

  3. 強相關的處理用線程, 弱相關的處理用進程 什麼叫強相關、弱相關?理論上很難定義, 給個簡單的例子就明白了. 通常的server須要完成以下任務:消息收發和消息處理. 消息收發和消息處理就是弱相關的任務, 而消息處理裏面可能又分爲消息解碼、業務處理, 這兩個任務相對來講相關性就要強多 了. 所以消息收發和消息處理能夠分進程設計, 消息解碼和業務處理能夠分線程設計.

  4. 可能擴展到多機分佈的用進程, 多核分佈的用線程

總結一下就是IO密集型通常使用多線程或者多進程, CPU密集型通常使用多進程, 強調非阻塞異步併發的通常都是使用協程, 固然有時候也是須要多進程線程池結合的, 或者是其餘組合方式. Nginx 用一個進程能夠跑滿整個機器的 cpu(都是非阻塞的操做, 沒有比這更快的了, Nginx 最近也引入了線程池, 對付會阻塞的任務),


分割線


Continuation

在計算機科學和程序設計中, 延續性(continuation)是一種對程序控制流程/狀態的抽象表現形式. 延續性使程序狀態信息具體化, 也能夠理解爲, 一個延續性以數據結構的形式表現了程序在運行過程當中某一點的計算狀態, 相應的數據內容能夠被編程語言訪問, 不被運行時環境所隱藏掉. 延續性包含了當前程序的棧(包括當前週期內的全部數據, 也就是本地變量), 以及當前運行的位置. 一個延續的實例能夠在未來被用作控制流, 被調用時它從所表達的狀態開始恢復執行.

協程wiki

適用於實現彼此熟悉的模塊:合做式多任務, 迭代器, 無限列表和管道.

var q := new queue

生產者協程

loop
    while q is not full
        create some new items
        add the items to q
    yield to consume

消費者協程

loop
    while q is not empty
        remove some items from q
        use the items
    yield to produce

每一個協程在用yield命令向另外一個協程交出控制時都儘量作了更多的工做. 放棄控制使得另外一個例程從這個例程中止的地方開始, 但由於如今隊列被修改了因此他能夠作更多事情. 儘管這個例子經常使用來介紹多線程, 實際沒有必要用多線程實現這種動態:yield語句能夠經過由一個協程向另外一個協程直接分支的方式實現.

注意 在yield的時候,隊列已經被改變了,下一個生產者(或者消費者)執行的時候,直接在這個中間狀態上執行就行了.

詳細比較

由於相對於子例程, 協程能夠有多個入口和出口點, 能夠用協程來實現任何的子例程. 事實上, 正如Knuth所說:「子例程是協程的特例. 」 每當子例程被調用時, 執行從被調用子例程的起始處開始;然而, 接下來的每次協程被調用時, 從協程返回(或yield)的位置接着執行. 由於子例程只返回一次, 要返回多個值就要經過集合的形式. 這在有些語言, 如Forth裏很方便;而其餘語言, 如C, 只容許單一的返回值, 因此就須要引用一個集合. 相反地, 由於協程能夠返回屢次, 返回多個值只須要在後繼的協程調用中返回附加的值便可. 在後繼調用中返回附加值的協程常被稱爲產生器. 子例程容易實現於堆棧之上, 由於子例程將調用的其餘子例程做爲下級. 相反地, 協程對等地調用其餘協程, 最好的實現是用continuations(由有垃圾回收的堆實現)以跟蹤控制流程.

與subroutine(子例程 函數?)比較

當一個subroutine被invoked,execution begins at the start,當它退出時就結束.一個subroutine的實例值返回一次,不持有狀態between invocation.

coroutine能夠退出經過調用其餘coroutine,可能過會會回到在原coroutine調用的那個point(which may return to the point where they were invoked in the original coroutine).從coroutine的視角來看,它並無退出,只是調用了其餘的coroutine(yield).所以,一個coroutine實例holds了state,而且在調用之間會有不一樣.

兩個coroutine yield to each other是對稱的(平等的),並且不是調用和被調用的關係(caller-callee 像函數的調用棧?)

全部的subroutine均可以被認爲是沒有yield的coroutine

要實現subroutine只須要一個簡單的棧,能夠預先分配,當程序執行的時候.可是coroutine要對等的調用對方,最好的實現是使用continuation

與generators相比

Generators,也叫semicoroutines.

var q := new queue

generator produce
    loop
        while q is not full
            create some new items
            add the items to q
        yield consume
generator consume
    loop
        while q is not empty
            remove some items from q
            use the items
        yield produce
subroutine dispatcher
    var d := new dictionary(generator → iterator)
    d[produce] := start produce
    d[consume] := start consume
    var current := produce
    loop
        current := next d[current]

與相互遞歸相比

Using coroutines for state machines or concurrency is similar to using mutual recursion with tail calls

coroutine 使用yield而不是返回,能夠複用execution,而不是重頭開始運行.

遞歸必須使用共享變量或者參數傳入狀態,每個遞歸的調用都須要一個新的棧幀.而在coroutine之間的passing control可使用現存的contexts,能夠簡單的被實現爲一個jump

coroutines yield rather than return, and then resume execution rather than restarting from the beginning, they are able to hold state, both variables (as in a closure) and execution point, and yields are not limited to being in tail position;

(好久之前的博客了, 不是原創, 參考了不少資料, 加着本身的理解)

相關文章
相關標籤/搜索