【轉載】 風格之爭:Coroutine模型 vs 非阻塞/異步IO(callback)

原帖地址: http://www.kuqin.com/system-analysis/20110910/264592.html


我們在設計一個服務器的軟件架構的時候,通常會考慮幾種架構:多進程,多線程,非阻塞/異步IO(callback) 以及Coroutine模型。

多進程
這種模型在linux下面的服務程序廣泛採用,比如大名鼎鼎的apache。主進程負責監聽和管理連接,而具體的業務處理都會交給子進程來處理。這裏有一篇我以前寫的文章具體的解釋這種架構的實現。

這種架構的最大的好處是隔離性,子進程萬一crash並不會影響到父進程。缺點就是對系統的負擔過重,想像一下如果有上萬的連接,會需要多少進程來處理。所以這種模型比較合適那種不需要太多併發量的服務器程序。另外,進程間的通訊效率也是一個瓶頸之一,大部分會採用share memory等技術來減低通訊開銷。

多線程
這種模型在windows下面比較常見。它使用一個線程來處理一個client。他的好處是編程簡單,最重要的是你會有一個清晰連續順序的work flow。簡單意味着不容易出錯。

這種模型的問題就是太多的線程會減低軟件的運行效率。

多進程和多線程都有資源耗費比較大的問題,所以在高併發量的服務器端使用並不多。這裏我們重點來研究一下兩種架構,基於callback和coroutine的架構。

Callback- 非阻塞/異步IO
這種架構的特點是使用非阻塞的IO,這樣服務器就可以持續運轉,而不需要等待,可以使用很少的線程,即使只有一個也可以。需要定期的任務可以採取定時器來觸發。把這種架構發揮到極致的就是node.js,一個用javascript來寫服務器端程序的框架。在node.js中,所有的io都是non-block的,可以設置回調。

舉個例子來說明一下。

傳統的寫法:
 var file = open(‘my.txt’);
 var data = file.read(); //block
 sleep(1);
 print(data); //block

node.js的寫法:

 fs.open(‘my.txt’,function(err,data){
    setTimeout(1000,function(){
       console.log(data);
    }
 }); //non-block

這種架構的好處是performance會比較好,缺點是編程複雜,把以前連續的流程切成了很多片段。另外也不能充分發揮多核的能力。

Coroutine-協程
coroutine本質上是一種輕量級的thread,它的開銷會比使用thread少很多。多個coroutine可以按照次序在一個thread裏面執行,一個coroutine如果處於block狀態,可以交出執行權,讓其他的coroutine繼續執行。使用coroutine可以以清晰的編程模型實現狀態機。讓我們看看Lua語言的coroutine的例子。

> function foo()
 >>   print("foo", 1)
 >>   coroutine.yield()
 >>   print("foo", 2)
 >> end
> co = coroutine.create(foo) -- create a coroutine with foo as the entry
> = coroutine.status(co)
 suspended
 > = coroutine.resume(co) <--第一次resume
 foo     1
 > = coroutine.resume(co) <--第二次resume
 foo     2
 > = coroutine.status(co)
 dead

Google go語言也對coroutine使用了語言級別的支持,使用關鍵字go來啓動一個coroutine(從這個關鍵字可以看出Go語言對coroutine的重視),結合chan(類似於message queue的概念)來實現coroutine的通訊,實現了Go的理念 ”Do not communicate by sharing memory; instead, share memory by communicating.”。一個例子:

func ComputeAValue(c chan float64) {
     // Do the computation.
     x := 2.3423 / 534.23423;
     c <- x;
 }
func UseAGoroutine() {
    channel := make(chan float64);
    go ComputeAValue(channel);
    // do something else for a while
    value := <- channel;
    fmt.Printf("Result was: %v", value);
 }

coroutine的優點是編程簡單,流程清晰。但是內存消耗會比較大,畢竟每個coroutine要保留當前stack的一些內容,以便於恢復執行。

Callback vs Coroutine
在業務流程實現上,coroutine確實是更理想的實現,基於callback的風格,代碼確實不是那麼清晰,你可能會寫出這樣的代碼。

//pseudo code
 socket.read(function(data){
 if(data==’1’)
   db.query(data,function(res){
     socket.write(res,function(){
       socket.read(function(data){
    });
   });
 });
 else
    doSomethingelse(...);
 });

當然你可以使用獨立的function函數來代替匿名的函數獲得更好的可讀性。如果使用coroutine就獲得比較清晰的模型。

 //pseudo code
 coroutine handle(client){
   var data = read(client); //coroutine will yield when read, resume when complete
    if(data==’1’){
    res = db.query(data);
   …
  }
  else{
    doSomething();
  }
 }

但是現實世界中,coroutine到目前爲止並沒有真正流行起來,第一,是因爲支持的語言並不是很多,比較新的語言(python/lua/go/erlang)才支持,但是老一些的語言(java/c/c++)並沒有語言級別的支持。第二個原因是因爲coroutine的使用可能讓一些糟糕的代碼佔用過多的內存,而且比較難於排查。另外在實現一個工作流的構架中,流的暫停和恢復的時機都是未知的,系統的狀態並不能放在內存中存放,都必須序列化,所以coroutine本身要提供序列化的機制,纔可以實現穩定的系統。我想這些就是coroutine叫好不叫座的原因。

儘管有很多人要求node.js實現coroutine,Node.js的作者Ryan dahl在twitter上說: ”i’m not adding coroutines. stop asking”.至於他的理由,他在google group上提到:

Yes, there have been discussions. I think, I won’t be adding
coroutines. I don’t like the added machinery and I feel that if I add
them, then people will actually use them, and then they’ll benchmark
their programs and think that node is slow and/or hogging memory. I’d
rather force people into designing their programs efficiently with
callbacks.

我想這是一種風格的選擇,優劣並不是絕對的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章