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的参数,参数传递将是下一节的主题。
文章转自庄周梦蝶 ,原文发布时间
2010-03-11
相关资源:敏捷开发V1.0.pptx