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的参数,参数传递将是下一节的主题。