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