fiber/纖程
在作業系統中,除了行程和執行緒外,還有一種較少應用的纖程(fiber,也叫協程)。纖程常常拿來跟執行緒做對比,對於作業系統而言,它們都是較輕量級的運行態。通常認為纖程比執行緒更為輕量,開銷更小。不同之處在於,纖程是由執行緒或纖程創建的,纖程調度完全由使用者程式碼控制,對系統核心而言,是一種非搶佔性的調度方式,纖程實現了合作式的多任務;而執行緒和進程則受核心調度,依照優先權,實現了搶佔式的多任務。另外,系統核心是不知道纖程的具體運作狀態,纖程的使用其實是比較與作業系統無關。
在node中,單執行緒是僅針對javascript而言的,其底層其實充斥著多執行緒。而如果需要在javascript中實作多線程,一種常見的做法是寫C addon,繞過javascript的單線程機制。不過這種方法提升了開發調試的難度和成本。像其他很多腳本語言,我們也可以把纖程的概念引入node。
node-fibers
node-fibers這個函式庫就為node提供了纖程的功能。多執行緒方面沒有測試出理想的結果,不過在非同步轉同步作用顯著,也許在減少node呼叫堆疊、無限遞迴方面也會有價值可挖。本文檔主要介紹 node-fibers函式庫的使用方法和非同步轉同步等內容。
安裝
node-fibers是採用C語言編寫,直接下載原始碼需要編譯,通常直接npm安裝即可:
fibers函式庫的使用
API
1.Fiber(fn)/ new Fiber(fn):
建立一個纖程,可以當成建構函式使用,也可以當成普通函式呼叫。如下例:
當 run()呼叫的時候,纖程啟動,並為 fn分配新的堆疊, fn會在這個新的堆疊上運行,直到 fn有回傳值或呼叫 yield()。 fn返回後或呼叫 yield()後,堆疊重置,當再次呼叫 run()時,纖程會再次啟動, fn運行於首次分配的堆疊中。
2.Fiber.current:
取得目前纖程,並可對其進行操作。如果指定一個變數與其相關聯,請務必確保此纖程能夠釋放,否則V8的垃圾回收機制會一直忽略這部分的內存,造成內存洩漏。
3.Fiber.yield(param):
前面的說明已經提過這個函數。 yield()方法用於中斷纖程,一定程度上類似 return。一旦執行 yield(),則此 Fiber中後續程式碼將沒有機會執行,例如:
執行後只會輸出“Fiber Start”,後者輸出指令沒有執行。如果向 yield()傳入參數,那麼此參數作為 run()的回傳值。
4.Fiber.prototype.run(param):
這個方法已經很熟悉了,之前隱約有提及呼叫 run()的兩種時態,一是Fiber未啟動時,一時Fiber被yield時。在這兩種時態下, run()的行為並不太一樣。
當Fiber未啟動時, run()接受一個參數,並把它傳遞給 fn,作為其參數。當Fiber處理yielding狀態時, run()接受一個參數,並把它當作 yield()的回傳值,fn並不會從頭運行,而是從中斷處繼續運作。關於 fn、 yield、 run三者的參數、回傳值等關係,可以用下面的小例子來說明:
輸出如下:
從上面例子中,可以很明顯看出 yield的使用方法與現在的javascript的語法相當不同。在別的語言中(C#、Python等)已經實作了 yield關鍵字,作為迭代器的中斷。不妨在node上也實作一個迭代器,具體體會一下 yield的使用。還是以開頭的斐波那契數作為例:
輸出為:
有兩個問題需要留意,第一, yield說是方法,更多地像關鍵字,與 run不同, yield不需要依托Fiber實例,而 run則需要。如果在Fiber內部呼叫run,則一定要使用: Fiber.current.run();第二, yield本身為javascript的保留關鍵字,不確定是否會、何時會啟用,所以程式碼在將來可能會面臨變更。
5.Fiber.prototype.reset():
我們已經知道Fiber可能存在不同的時態,同時會影響 run的行為。而 reset方法則不管Fiber處理什麼狀態,都恢復到初始狀態。隨後再執行 run,就會重新執行 fn。
6.Fiber.prototype.throwInto(Exception):
本質上 throwInto會拋出傳給它的例外,並將例外訊息作為 run的回傳值。如果在Fiber內不對它拋出的異常作處理,則異常會繼續冒泡。不管異常是否處理,它會強制 yield,中斷Fiber。
future庫的使用
在node中直接使用Fiber並不總是合理的,因為Fiber的API實在簡單,實際使用中難免會產生重複冗長的程式碼,不利於維護。推薦在node與Fiber之間增加一層抽象,讓Fiber能夠更好地運作。 future庫就提供了這樣一種抽象。 future函式庫或任何一層抽象化也許都不是完美的,沒有誰對誰錯,只有適用不適用。例如, future函式庫向我們提供了簡單的API能夠完成非同步轉同步的工作,然而它對封裝 generator (類似上面的斐波那契數列產生器)則無能為力。
future庫不需要單獨下載安裝,已經包含在 fibers庫中,使用時只需要 var future=require('fibers/future') 即可。
API
1.Function.prototype.future():
為 Function類型加入了 future方法,將function轉換成一個「funture-function」。
實際上 power方法是在Fibel內執行的。不過現有版本的 future有bug,官方沒有具體的說明,如果需要使用此功能,請刪除掉 future.js的第339行和第350行。
2.new Future()
Future物件的建構函數,下文將詳細介紹。
3.Future.wrap(fn, idx)
wrap方法封裝了非同步轉同步的操作,是 future庫中對我們最有價值的方法。 fn表示需要轉換的函數, idx表示fn接受的參數數目,認為其callback方法為最後一個參數(這邊API的製定頗有爭議,有人傾向傳遞callback應該處於的位置,好在wrap方法比較簡單,可以比較容易修改程式碼)。看一個例子就能了解 wrap的用法:
從這個範例可以看出Fiber非同步轉同步確實非常有效,除了語法上多了一步 .wait()外,其他已經 fs提供的 fs.readFileSync方法別無二致了。
4.Future.wait(futures):
這個方法前面已經多次看到了。顧名思義,它的作用就是等待結果。如果要等待一個future的實例的結果,直接呼叫 futureInstance.wait()即可;如果需要等待一系列future實例的結果,則呼叫 Future.wait(futuresArray)。要注意的是,在第二種用法中,一個future實例在運行時出現錯誤, wait方法不會拋出錯誤,不過我們可以使用 get()方法直接取得運行結果。
5.Future.prototype.get():
get()的用法與 wait()的第一種方式很像,不同的是, get()立刻回傳結果。如果資料還沒準備好, get()會拋出錯誤。
6.Future.prototype.resolve(param1,param2):
上面的 wrap方法總是給人以一種 future其實在吞噬非同步方法的回呼函數,並直接傳回非同步結果。事實上 future也透過 resolve方法提供設定回呼函數的解決方案。 resolve最多接受兩個參數,如果只傳入一個參數, future認為傳了一個node風格的回呼函數,例如如下例:
2 つのパラメーターが渡された場合、エラーとデータが別々に処理されることを意味します。例は次のとおりです。
さらに、future は、resolve の呼び出しタイミングを区別しません。データが準備できていない場合、コールバック関数はキューにプッシュされ、resolver() メソッドによって一律にスケジュールされます。コールバック関数はすぐに実行されます。
7.Future.prototype.isResolved():
操作が実行されたかどうかを示すブール値を返します。
8.Future.prototype.proxy(futureInstance):
プロキシ メソッドは、将来のインスタンスにプロキシを提供します。これは、本質的には、resolve メソッドのラッパーです。実際、あるインスタンスのコールバック メソッドを、別のインスタンスのコールバック エグゼキュータとして使用します。例:
プロキシが実行されますが、最終的にはターゲットのコールバック関数が実行され、プロキシの実行結果がターゲットのコールバック関数を駆動します。このプロキシ手法は実際のアプリケーションで大きな役割を果たす可能性がありますが、まだ深く考えていません。
9.Future.prototype.return(value):
10.Future.prototype.throw(error):
11.Future.prototype.resolver():
12.Future.prototype.detach():
上記 4 つの API については、他の API に比べて、実際の利用シーンや機能は比較的平均的だと感じます。 return と throw はどちらもリゾルバー メソッドによってスケジュールされており、これら 3 つのメソッドは非常に重要であり、通常の今後の使用プロセスで静かに動作します。しかし、これらを個別に使用するための具体的なシナリオが思い浮かばないため、導入する方法がありません。詳細に。 detach メソッドは、resolve メソッドの簡易版とみなすことができ、導入する必要はありません。