1. 程式人生 > >協程(四)原理

協程(四)原理

協程,又稱微執行緒和纖程等,據說源於 Simula 和 Modula-2 語言(我沒有深究,有錯請指正),現代程式語言基本上都有支援,比如 Lua、ruby 和最新的 Google Go,當然也還有最近很讓我驚豔的 falcon。協程是使用者空間執行緒,作業系統其存在一無所知,所以需要使用者自己去做排程,用來執行協作式多工非常合適。其實用協程來做的東西,用執行緒或程序通常也是一樣可以做的,但往往多了許多加鎖和通訊的操作。

下面是生產者消費者模型的基於搶佔式多執行緒程式設計實現(虛擬碼):
// 佇列容器
var q := new queue
// 消費者執行緒
loop
lock(q)
get item from q
unlock(q)
if item
use this item
else
sleep 
// 生產者執行緒
loop
create some new items
lock(q)
add the items to q
unlock(q)

由以上程式碼可以看到執行緒實現至少有兩點硬傷:

1、對佇列的操作需要有顯式/隱式(使用執行緒安全的佇列)的加鎖操作。

2、消費者執行緒還要通過 sleep 把 CPU 資源適時地“謙讓”給生產者執行緒使用,其中的適時是多久,基本上只能靜態地使用經驗值,效果往往不由人意。

而使用協程可以比較好的解決這個問題,下面來看一下基於協程的生產者消費者模型實現(虛擬碼):
// 佇列容器
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

可以從以上程式碼看到之前的加鎖和謙讓 CPU 的硬傷不復存在,但也損失了利用多核 CPU 的能力。所以選擇執行緒還是協程,就要看應用場合了。下面簡單談一下協程常見的用武之地,其中之一是狀態機,能夠產生更高可讀性的程式碼;還有就是並行的角色模型,這在遊戲開發中比較常見;以及產生器, 有助於對輸入/輸出和資料結構的通用遍歷。

協程雖然如此之好,看是很長時間以來,因為受到基於堆疊的子例程實現的限制,並沒有多少語言在其實語言或庫中支援協程,所以執行緒作為一個替代者(當然,執行緒也有其超越協程之處)被廣泛接受了。但是在今天,很多語言都內建了協程的支援,甚至是 C/C++ 語言。MS Windows 2000 以後的版本,都支援所謂的 Fiber,即纖程,其實就是協程的別稱;在開源平臺,POSIX 標準也定義了協程相關的標準,GNU Portable Threads 實現了跨平臺的使用者空間執行緒,即協程的另一種別稱。在這百花齊放的時節,正是我們好好學習和利用它的時機。

接下來我將在第二篇中談談遊戲中試用協程的三個場合。


協程,又稱微執行緒和纖程等,據說源於 Simula 和 Modula-2 語言(我沒有深究,有錯請指正),現代程式語言基本上都有支援,比如 Lua、ruby 和最新的 Google Go,當然也還有最近很讓我驚豔的 falcon。協程是使用者空間執行緒,作業系統其存在一無所知,所以需要使用者自己去做排程,用來執行協作式多工非常合適。其實用協程來做的東西,用執行緒或程序通常也是一樣可以做的,但往往多了許多加鎖和通訊的操作。

下面是生產者消費者模型的基於搶佔式多執行緒程式設計實現(虛擬碼):
// 佇列容器
var q := new queue
// 消費者執行緒
loop
lock(q)
get item from q
unlock(q)
if item
use this item
else
sleep 
// 生產者執行緒
loop
create some new items
lock(q)
add the items to q
unlock(q)

由以上程式碼可以看到執行緒實現至少有兩點硬傷:

1、對佇列的操作需要有顯式/隱式(使用執行緒安全的佇列)的加鎖操作。

2、消費者執行緒還要通過 sleep 把 CPU 資源適時地“謙讓”給生產者執行緒使用,其中的適時是多久,基本上只能靜態地使用經驗值,效果往往不由人意。

而使用協程可以比較好的解決這個問題,下面來看一下基於協程的生產者消費者模型實現(虛擬碼):
// 佇列容器
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

可以從以上程式碼看到之前的加鎖和謙讓 CPU 的硬傷不復存在,但也損失了利用多核 CPU 的能力。所以選擇執行緒還是協程,就要看應用場合了。下面簡單談一下協程常見的用武之地,其中之一是狀態機,能夠產生更高可讀性的程式碼;還有就是並行的角色模型,這在遊戲開發中比較常見;以及產生器, 有助於對輸入/輸出和資料結構的通用遍歷。

協程雖然如此之好,看是很長時間以來,因為受到基於堆疊的子例程實現的限制,並沒有多少語言在其實語言或庫中支援協程,所以執行緒作為一個替代者(當然,執行緒也有其超越協程之處)被廣泛接受了。但是在今天,很多語言都內建了協程的支援,甚至是 C/C++ 語言。MS Windows 2000 以後的版本,都支援所謂的 Fiber,即纖程,其實就是協程的別稱;在開源平臺,POSIX 標準也定義了協程相關的標準,GNU Portable Threads 實現了跨平臺的使用者空間執行緒,即協程的另一種別稱。在這百花齊放的時節,正是我們好好學習和利用它的時機。

接下來我將在第二篇中談談遊戲中試用協程的三個場合。


作者:阿貓
連結:http://www.zhihu.com/question/20511233/answer/24260355
來源:知乎
著作權歸作者所有,轉載請聯絡作者獲得授權。

沒有啥複雜的東西,考慮清楚需求,就可以很自然的衍生出這些解決方案。
  • 一開始大家想要同一時間執行那麼三五個程式,大家能一塊跑一跑。特別是UI什麼的,別一上計算量比較大的玩意就跟宕機一樣。於是就有了併發,從程式設計師的角度可以看成是多個獨立的邏輯流。內部可以是多cpu並行,也可以是單cpu時間分片,能快速的切換邏輯流,看起來像是大家一塊跑的就行。
  • 但是一塊跑就有問題了。我計算到一半,剛把多次方程解到最後一步,你突然插進來,我的中間狀態咋辦,我用來儲存的記憶體被你覆蓋了咋辦?所以跑在一個cpu裡面的併發都需要處理上下文切換的問題。程序就是這樣抽象出來個一個概念,搭配虛擬記憶體、程序表之類的東西,用來管理獨立的程式執行、切換。
  • 後來一電腦上有了好幾個cpu,好咧,大家都別閒著,一人跑一程序。就是所謂的並行
  • 因為程式的使用涉及大量的計算機資源配置,把這活隨意的交給使用者程式,非常容易讓整個系統分分鐘被搞跪,資源分配也很難做到相對的公平。所以核心的操作需要陷入核心(kernel),切換到作業系統,讓老大幫你來做。
  • 有的時候碰著I/O訪問,阻塞了後面所有的計算。空著也是空著,老大就直接把CPU切換到其他程序,讓人家先用著。當然除了I\O阻塞,還有時鐘阻塞等等。一開始大家都這樣弄,後來發現不成,太慢了。為啥呀,一切換程序得反覆進入核心,置換掉一大堆狀態。程序數一高,大部分系統資源就被程序切換給吃掉了。後來搞出執行緒的概念,大致意思就是,這個地方阻塞了,但我還有其他地方的邏輯流可以計算,這些邏輯流是共享一個地址空間的,不用特別麻煩的切換頁表、重新整理TLB,只要把暫存器重新整理一遍就行,能比切換程序開銷少點。
  • 如果連時鐘阻塞、 執行緒切換這些功能我們都不需要了,自己在程序裡面寫一個邏輯流排程的東西。那麼我們即可以利用到併發優勢,又可以避免反覆系統呼叫,還有程序切換造成的開銷,分分鐘給你上幾千個邏輯流不費力。這就是使用者態執行緒
  • 從上面可以看到,實現一個使用者態執行緒有兩個必須要處理的問題:一是碰著阻塞式I\O會導致整個程序被掛起;二是由於缺乏時鐘阻塞,程序需要自己擁有排程執行緒的能力。如果一種實現使得每個執行緒需要自己通過呼叫某個方法,主動交出控制權。那麼我們就稱這種使用者態執行緒是協作式的,即是協程

本質上協程就是使用者空間下的執行緒。