Ruby Fiber指南(一)基礎
Ruby Fiber指南(二)參數傳遞
Ruby Fiber指南(三)過濾器
Ruby Fiber指南(四)迭代器
Ruby Actor指南(五)實現Actor
這是一個Ruby Fiber的教程,基本是按照《Programming in lua》中講述協程章節的順序來介紹Ruby Fiber的,初步分為5節:基礎、參數傳遞、過濾器、迭代器、應用。這是第一節,介紹下Ruby Fiber的基礎知識。
Ruby 1.9引入了Fiber,通常稱為纖程,事實上跟傳統的coroutine——協程是一個概念,一種非搶占式的多線程模型。所謂非搶占式就是當一個協程運行的時候,你不能在外部終止它,而只能等待這個協程主動(一般是yield)讓出執行權給其他協程,通過協作來達到多任務并發的目的。協程的優點在于由于全部都是用戶空間內的操作,因此它是非常輕量級的,占用的資源很小,并且context的切換效率也非常高效(可以看看
這個測試),在編程模型上能簡化對阻塞操作或者異步調用的使用,使得涉及到此類操作的代碼變的非常直觀和優雅;缺點在于容錯和健壯性上需要做更多工作,如果某個協程阻塞了,可能導致整個系統掛住,無法充分利用多核優勢,有一定的學習使用曲線。
上面都是場面話,先看看代碼怎么寫吧,比如我們寫一個打印hello的協程:
1 require 'fiber'
2 f=Fiber.new do
3 p "hello"
4 end
5
6 p f.alive?
7 f.resume
8 p f.alive?
9
10 f.resume
11
附注:這里的代碼都在ruby1.9.1-p378測試通過。
第一行先引入fiber庫,事實上fiber庫并不是必須的,這里是為了調用Fiber#alive?方法才引入。然后通過Fiber#new創建一個Fiber,Fiber#new接受一個block,block里就是這個Fiber將要執行的任務。Fiber#alive?用來判斷Fiber是否存活,一個Fiber有三種狀態:Created、Running、Terminated,分別表示創建完成、執行、終止,處于Created或者Running狀態的時候Fiber#alive?都返回true。啟動Fiber是通過Fiber#resume方法,這個Fiber將進入Running狀態,打印"hello"并終止。當一個Fiber終止后,如果你再次調用resume將拋出異常,告訴你這個Fiber已經壽終正寢了。因此上面的程序輸出是:
0
"hello"
false
fiber1.rb:10:in `resume': dead fiber called (FiberError)
from fiber1.rb:10:in `<main>'
眼尖的已經注意到了,這里alive?返回是0,而不是true,這是1.9.1這個版本的
一個BUG,1.9.2返回的就是true。不過在Ruby里,除了nil和false,其他都是true。
剛才提到,我們為了調用Fiber#alive?而引入了fiber庫,Fiber其實是內置于語言的,并不需要引入額外的庫,fiber庫對Fiber的功能做了增強,具體可以先看看它的
文檔,主要是引入了幾個方法:Fiber#current返回當前協程,Fiber#alive?判斷Fiber是否存活,最重要的是Fiber#transfer方法,這個方法使得Ruby的Fiber支持所謂全對稱協程(
symmetric coroutines),默認的resume/yield(yield后面會看到)是半對稱的協程(asymmetric coroutines),這兩種模型的區別在于“掛起一個正在執行的協同函數”與“使一個被掛起的協同再次執行的函數”是不是同一個。在這里就是Fiber#transfer一個方法做了resume/yield兩個方法所做的事情。全對稱協程就可以從一個協程切換到任意其他協程,而半對稱則要通過調用者來中轉。但是Ruby Fiber的調用不能跨線程(thread,注意跟fiber區分),只能在同一個thread內進行切換,看下面代碼:
1 f = nil
2 Thread.new do
3 f = Fiber.new{}
4 end.join
5 f.resume
f在線程內創建,在線程外調用,這樣的調用在Ruby 1.9里是不允許的,執行的結果將拋出異常
fiber_thread.rb:5:in `resume': fiber called across threads (FiberError)
from fiber_thread.rb:5:in `<main>'
剛才我們僅僅使用了resume,那么yield是干什么的呢?resume是使一個掛起的協程執行,那么yield就是讓一個正在執行的Fiber掛起并將執行權交給它的調用者,yield只能在某個Fiber任務內調用,不能在root Fiber調用,程序的主進程就是一個root fiber,如果你在root fiber執行一個Fiber.yield,也將拋出異常:
Fiber.yield
FiberError: can't yield from root fiber
看一個resume結合yield的例子:
1 f=Fiber.new do
2 p 1
3 Fiber.yield
4 p 2
5 Fiber.yield
6 p 3
7 end
8
9 f.resume # =>打印1
10 f.resume # => 打印2
11 f.resume # =>打印3
f是一個Fiber,它的任務就是打印1,2,3,第一次調用resume時,f在打印1之后調用了Fiber.yield,f將讓出執行權給它的調用者(這里就是root fiber)并掛起,然后root fiber再次調用f.resume,那么將從上次掛起的地方繼續執行——打印2,又調用Fiber.yield再次掛起,最后一次f.resume執行后續的打印任務并終止f。
Fiber#yield跟語言中的yield關鍵字是不同的,block中的yield也有“讓出”的意思,
但是這是在同一個context里,而Fiber#yield讓出就切換到另一個context去了,這是完全不同的。block的yield其實是匿名函數的語法糖衣,它是切換context的,跟Fiber不同的是,它不保留上一次調用的context,這個可以通過一個例子來區分:
1 def test
2 yield
3 yield
4 yield
5 end
6 test{x ||= 0; puts x+= 1}
7
這里的test方法接受一個block,三次調用yield讓block執行,block里先是初始化x=0,然后每次調用加1,你期望打印什么?
答案是:
1
1
1
這個結果剛好證明了yield是不保留上一次調用的context,每次x都是重新初始化為0并加上1,因此打印的都是1。讓我們使用Fiber寫同一個例子:
1 fiber=Fiber.new do
2 x||=0
3 puts x+=1
4 Fiber.yield
5 puts x+=1
6 Fiber.yield
7 puts x+=1
8 Fiber.yield
9 end
10
11 fiber.resume
12 fiber.resume
13 fiber.resume
14
執行的結果是:
1
2
3
這次能符合預期地打印1,2,3,說明Fiber的每次掛起都將當前的context保存起來,留待下次resume的時候恢復執行。因此關鍵字yield是無法實現Fiber的,fiber其實跟continuation相關,在底層fiber跟callcc的實現是一致的(cont.c)。
Fiber#current返回當前執行的fiber,如果你在root fiber中調用Fiber.current返回的就是當前的root fiber,一個小例子:
1 require 'fiber'
2 f=Fiber.new do
3 p Fiber.current
4 end
5
6 p Fiber.current
7 f.resume
這是一次輸出:
#<Fiber:0x9bf89f4>
#<Fiber:0x9bf8a2c>
表明root fiber跟f是兩個不同的Fiber。
基礎的東西基本講完了,最后看看Fiber#transfer的簡單例子,兩個協程協作來打印“hello world”:
1 require 'fiber'
2
3 f1=Fiber.new do |other|
4 print "hello"
5 other.transfer
6 end
7
8 f2=Fiber.new do
9 print " world\n"
10 end
11
12 f1.resume(f2)
通過這個例子還可以學到一點,resume可以傳遞參數,參數將作為Fiber的block的參數,參數傳遞將是下一節的主題。