怎麼理解coroutine ?

coroutine 和多線程的概念搞不太清楚,在網上也搜不到很好的文章解釋這個概念,哪位高手能解釋一下?


用戶控制切換,自動保存上下文狀態,切換之間可以通過參數通信,可用同步的方式實現非同步。這是我理解中的協程最重要的幾個特徵。


我剛剛寫了一篇關於coroutine的博客,順便把內容分享過來吧

排版還是不好,在博客上有詳細版:理解Lua中最強大的特性-coroutine(協程)

coroutine基礎

Lua所支持的協程全稱被稱作協同式多線程(collaborative multithreading)。Lua為每個coroutine提供一個獨立的運行線路。然而和多線程不同的地方就是,coroutine只有在顯式調用yield函數後才被掛起,同一時間內只有一個協程正在運行。

Lua將它的協程函數都放進了coroutine這個表裡,其中主要的函數如下

摘取一段雲風的代碼來詳盡解釋協程的工作機制,在這段代碼中,展示了main thread和協程co之間的交互:

function foo(a)
print("foo", a)
return coroutine.yield(2 * a)
end

co = coroutine.create(function ( a, b )
print("co-body", a, b)
local r = foo(a + 1)
print("co-body", r)
local r, s = coroutine.yield(a + b, a - b)
print("co-body", r, s)
return b, "end"
end)

print("main", coroutine.resume(co, 1, 10))
print("main", coroutine.resume(co, "r"))
print("main", coroutine.resume(co, "x", "y"))
print("main", coroutine.resume(co, "x", "y"))

下面是運行結果

co-body 1 10

foo 2

main true 4

co-body r

main true 11, -9

co-body x y

main false 10 end

main false cannot resume dead coroutine

coroutine和subroutine(子常式)的比較

子常式的起始處是唯一的入口點,一旦return就完成了子程序的執行,子程序的一個實例只會運行一次。

但是協程不一樣,協程可以使用yield來切換到其他協程,然後再通過resume方法重入(reenter)到上次調用yield的地方,並且把resume的參數當成返回值傳給了要重入(reenter)的協程。但是coroutine的狀態都沒有被改變,就像一個可以多次返回的subroutine。

coroutine的和callback的比較

coroutine經常被用來和callback進行比較,因為通常來說,coroutine和callback可以實現相同的功能,即非同步通信,比如說下面的這個例子:

bob.walto(function ( )
bob.say(function ( )
jane.say("hello")
end,"hello")
end, jane)

用coroutine則可以這麼實現

function runAsyncFunc( func, ... )
local current = coroutine.running
func(function ( )
coroutine.resume(current)
end, ...)
coroutine.yield()
end

coroutine.create(function ( )
runAsyncFunc(bob.walkto, jane)
runAsyncFunc(bob.say, "hello")
jane.say("hello")
end)

coroutine.resume(co)


coroutine你可以將它看成一個用戶態的線程(一般來它也提供了入口函數、調用的參數,以及你放置局部變數的棧),只不過它是你自己調度的,而且不同coroutine的切換不需要陷入內核態,效率比較高。

linux有提供了getcontext swapcontext等介面來實現coroutine,windows貌似也有相關的。一般來說coroutine用在非同步的場景比較好,非同步執行一般需要維護一個狀態機,狀態的維護需要保存在全局裡或者你傳進來的參數來,因為每一個狀態回調都會重新被調用。有了coroutine(stackfull)的話你可以不用擔心這個問題,你可以像寫同步的代碼那樣子,但其實底層還是非同步的,只不過你在等待數據時執行的上下文會暫時被保存起來,等到數據來臨再將上下文恢復繼續執行。還有一種coroutine是stackless,它本質上也是狀態機實現的,並不能在它上面讓不同的狀態共享局部變數,貌似boost.asio.coroutine就是這種。

這裡順便廣告一下:colaghost/coroutine_event · GitHub

這是基於libevent簡單封裝的一個為非同步IO提供同步訪問的庫,包裝了accept connect read write這幾個介面,同步的過程是利用coroutine的切換來實現的,有興趣可以看下。


我的理解,coroutine本身提供了一種機制,讓開發者可以控制一個代碼片段,讓它暫停,然後在下次調用時繼續從上次暫停的地方繼續運行。

這個機制有什麼用呢?

1. 簡化狀態機的編寫,如下面這個例子,比如一個開關有4個狀態,關閉-&>慢速-&>高速-&>超高速-&>關閉。每按一下狀態切換一下,可以用對象內部維護狀態,然後判斷的方式,也可以用coroutine的方式。對象模式已經做了簡化,沒考慮並行以及容錯的問題。

class Button:
state = 0

def push(self):
if self.state == 0:
self.state = 1
elif self.state == 1:
self.state = 2
elif self.state ==2:
self.state = 3
else:
self.state = 0
return self.state

def push_switch():
state = 0
while True:
state = 1
yield state
state = 2
yield state
state = 3
yield state
state = 0
yield state

def main():
btn = Button()
print("object switch")
print(btn.push())
print(btn.push())
print(btn.push())
print(btn.push())
print("coroutine switch")
btn2 = push_switch()
print(next(btn2))
print(next(btn2))
print(next(btn2))
print(next(btn2))

輸出結果:

object switch
1
2
3
0
coroutine switch
1
2
3
0

二者基本是等價的,但後者看起來明顯比前者更簡單些。更複雜的例子可以參看:Co-routines as an alternative to state machines

2. 實現用戶態的線程(GreenThread/Fiber)因為要實現用戶態的線程首先需要解決的問題就是要提供一種機制可以讓程序暫停/繼續,這樣才能實現調度。更詳細的分析可以看看本人這篇文章:並發之痛 Thread,Goroutine,Actor


從消費者心理學來講,哪些誘因促使消費者完成在線全款購車的?

之前寫的一篇介紹短文


http://www.cnblogs.com/foxmailed/p/3509359.html 之前寫的一篇博客


線程是操作系統級別的概念,現代操作系統都實現並且支持線程,線程的調度對應用開發者是透明的,開發者無法預期某線程在何時被調度執行。基於此,一般那種隨機出現的BUG,多與線程調度相關。

coroutine則是一個概念,windows上有所謂的fiber纖程實現,而好些語言中也自帶coroutine的實現,比如Lua。與線程最大的不同是,coroutine的調度/掛起/執行開發者是可以控制的。另外coroutine也比線程輕量的多。要在語言層面實現coroutine,需要內部有一個類似棧的數據結構,當該coroutine被掛起時要保存該coroutine的數據現場以便恢復執行。


關於協程,knuth解釋的最簡單明了,協程就是多入多出的子常式,另外從字面組成也非常切貼,就是可以協同運行的常式。

協程本身與線程沒有關係,只是如果從調度角度來看的話,相當於一種用戶級協作式調度方案(好像windows fiber不完全是協作式的,但我傾向於是協作式才能稱為協程)。至於goroutine,就是相當於把調度本身可以分配到多個線程中,或者叫非同步調度。我在自己的實現中稱之為concurrent coroutine(goroutine可能不能這麼叫,已知的協程實現中都不包含真正的並行成分)。

至於還有一些其他概念比如continuation,其實跟協程也沒太大關係,只是continuation可以用於實現協程。這是當然的,誰讓continuation是一切調用之母呢(is the mother of all function calls,類似有Continuation monad is the mother of all monads)。也有不基於continuation的實現,比如有人貼出的c實現,是基於duff"s devices的跳轉表(狀態機)實現。也可以把兩者結合起來用。

我實現了一個還算通用的continuation for c,並在此基礎上實現了完整的concurrent coroutine,可以參考:https://github.com/zhouzhenghui/c-sigslot,目前僅提交了continuation部分,包含一個closure實現,可以並行調用,暫未提交完整的concurrent部分以及event-driven部分。


多線程的解釋太多了,講下coroutine吧

coroutine 可以看成一個可以返回多次的函數,他可以計算到一半返回一個值,在上層指定的時候繼續往下算....

舉個例子,lua的代碼,計算f(n) = f(n-1) + f(n-2)的數列

fib = coroutine.create(
function ()
local x, y = 1, 1
while true do
coroutine.yield(x)
x, y = y, x + y
end
end)

for i = 1, 10 do
print(coroutine.resume(fib))
end

resume就相當於我們一般的函數調用

yield就相當於返回

這裡就類似調用了10次,但是每次都是從上次yield的地方往下執行

另外說一下,這個玩意好用在它每個coroutine有自己的棧,所以可以繼續進行函數調用。


協程是自己程序調度的,邏輯上並行執行,底層上非並行執行

線程是操作系統調度的,邏輯和底層上都是並行執行


A Curious Course on Coroutines and Concurrency

很好看


協程是在編譯器層面用軟體的方式實現的,線程是硬體中斷實現的,所以協程的切換不會引起線程的切換,線程的切換有它自己的機制,跟協程無關,但是協程是站在線程的基礎上實現運作的,沒有線程就不可能實現協程。從實現的效果來看,兩者是類似的,都是程序在運行一段時間後保存現場然後跳轉執行另一段程序,區別在於程序員可以在協程指定何處進行跳轉以及經歷多長時間再跳轉回來,相當於指定了協程的切換時間。而線程切換的時間一般是在操作系統內核時鐘中斷函數中指定的,程序員無法修改。


內核線程創建、銷毀、切換成本都比較搞,一般語言中線程阻塞後導致內核線程阻塞,及時達到了恢復的條件也要等待下一次CPU調度。coroutine是在語言層面實現的,可以在一個內核線程上去調度多個coroutine,由語言運行時來控制切換,可以更加靈活且切換成本更小。相當於加了一層抽象。


首先聲明一下,我是寫Java的(渣;

嘛,看了Scala的那啥Actor之後,從字面上看,感覺可以等效為

boolean flag=true;

new Actor(new Receiver[](data){

try{ /*if flag=true;*/

/*switch (this)*/

case(instanceof Actor0)

case(instanceof AnyActor){

proc(this);

continue;

}

}catch( UnmatchedException){ /*沒有這個異常啦*/

flag=false;

break;

}

});

void proc(Receiver rc){

System.out.println("matched:"+rc.data);

}

/*表示我習慣用一個介面Receiver表示所有處理的事務,再判斷它們屬於哪個類型,複寫對應的方法proc() ,回調的部分木有寫~歡迎拍磚*/


coroutine是可以在任意行中斷然後繼續的函數, 一般的函數routine只是coroutine的一種特殊情況。


協程的原理其他回答都講的差不多了,其實就是基於 glibc 的一組上下文切換函數實現的

getcontext() : 獲取當前context
setcontext() : 切換到指定context
makecontext() : 設置 函數指針 和 堆棧 到對應context保存的sp和pc寄存器中,調用之前要先調用 getcontext()
swapcontext() : 保存當前context,並且切換到指定context

我倒是覺得雲風的庫里最難理解的部分是協程運行棧的保存和恢復

  1. 保存現場

協程在從 RUNNING 到 COROUTINE_SUSPEND 狀態時需要保存運行棧,即調用 coroutine_yield 之後掛起協程,讓出CPU的過程。coroutine_yield 調用 _save_stack 來保存運行棧:

static void _save_stack(struct coroutine *C, char *top) {
//獲取當前棧底,top 是棧頂,top-dummy 即該協程的私有棧空間
char dummy = 0;
assert(top - dummy &<= STACK_SIZE); //如果協程私有棧空間大小不夠放下運行時的棧空間,則要重新擴容 if (C-&>cap &< top - dummy) { free(C-&>stack);
C-&>cap = top-dummy;
C-&>stack = malloc(C-&>cap);
}
C-&>size = top - dummy;
//從棧底到棧頂的數據全都拷到 C-&>stack 中
memcpy(C-&>stack, dummy, C-&>size);
}

top 代表當前協程運行棧的棧頂,從 coroutine_yield 我們知道

top = S-&>stack + STACK_SIZE

為什麼是 S-&>stack + STACK_SIZE 呢,因為該協程在初始化的時候設置為:

C-&>ctx.uc_stack.ss_sp = S-&>stack;
C-&>ctx.uc_stack.ss_size = STACK_SIZE;
makecontext(C-&>ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr&>&>32));

即表示該協程棧的棧頂設置為 S-&>stack,mainfunc 的運行使用 S-&>stack 作為棧頂,大小為 STACK_SIZE 。

由此可以看到,schedule 的 stack[STACK_SIZE] 是子協程運行的公共棧空間,但是每個協程的棧不一樣,所以每個協程都有一個 C-&>stack 私有棧空間來保存執行現場。

top 表示棧頂,而dummy表示該用戶協程當前的棧底,所以 top-dummy 就表示該用戶協程運行時棧所佔空間。然後將運行時棧的數據全部拷貝到該協程的C-&>stack中

memcpy(C-&>stack, dummy, C-&>size);

2. 恢復現場

協程從 SUSPEND 到 RUNNING 狀態時,如何恢復當時的運行棧,由於C 的運行時棧空間始終是在 S-&>main中的,因此恢復棧空間,其實就是將各自私有的C-&>stack 空間中的數據恢復到S-&>main中:

memcpy(S-&>stack + STACK_SIZE - C-&>size, C-&>stack, C-&>size);

最後濃縮一下兩句關鍵代碼,保存和恢復要始終注意棧的生長方向

memcpy(C-&>stack, dummy, C-&>size);//保存
memcpy(S-&>stack + STACK_SIZE - C-&>size, C-&>stack, C-&>size); //恢復


大概是用戶態進行調度的程序片,我重點說下同步實現非同步這個事情。

如果把需要自己從用戶態調用io去讀取buffer稱為同步,自己開好buffer讓內核去填充稱為非同步,那麼,線程還是協程都無法實現同步方式寫非同步,只能依靠內核提供非同步io函數。

如果僅僅稱不需要一直阻塞到io可讀等事件,等待可讀時可以去調用io函數去處理稱為非同步。那麼多線程就可以實現了,不論你多少個同步io,開一堆線程去做就行了,這樣的同步方式寫非同步,與協程無關,就是線程本身的事情。

如果你要說goroutine做了什麼?對於socket io,它調用非阻塞io,使用epoll/iocp等循環取出ready socket,然後適時喚醒阻塞的goroutine,和直接用多線程處理有什麼區別?調度更輕量(不進內核態),啟動銷毀更快。對於文件io,是調用同步io,長系統調用進入前會告訴調度器我要阻塞了,然後新開(也可能只是從池中取)線程跑其他goroutine。然後阻塞返回之後把這個阻塞的線程搞回全局(性能可能不怎麼樣...linux的aio太差沒有用非同步,所以只好這樣),把線程殺了(或者放入池中)等待以後調度。


題主參見這篇文章

Lua coroutine 不一樣的多線程編程思路

Lua中實現coroutine的想法

下面這篇文章介紹Lua中coroutine的用法,可以有一個感官的認識

Programming in Lua : 9.1


看了一些資料,覺得如果簡單理解的話,就是用戶態的多線程,也就是多線程(多個函數)之間調度是在用戶態完成而不是內核態(native thread,類似Java的多線程)。


就是以前C里的fiber 只是實現方式不同


lz在比較兩個不同級別得概念(coroutine 和多線程),可以把 coroutine 看作一個特殊的函數(參見 Coroutines in C ),雖然 Coroutines 的典型應用場景(要解決的問題)涉及多線程和分散式。

從你的解釋,我理解的是coroutine並非是多線程的。最近在學習golang,它的goroutine是一個類似coroutine的實現,但是是一個多線程的實現。所以我對線程和coroutine還是覺得有點混亂。

另外,看到lz在 @colaghost 的回答下面問這個問題,其實 goroutine 和 coroutine 不是一個概念(Is golang goroutine a coroutine?)。現在感覺lz是在問 goroutine 和多線程的區別,如果是這樣,也可以參考前面的鏈接。


推薦閱讀:

php創建的文件夾名里含有「黒」字時,「黒」字較大概率會重複出現,請問是何原因?
為什麼國內入門書籍嚴重缺失?
RapidJSON中itoa的實現是現在已知最快的么?
如何寫好一個parser?
如何看待25歲博士雷霄驊猝死?

TAG:遊戲開發 | 編程 | 多線程 |