Ruby Fiber指南(一)基础

简介:
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  =  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 
   
目录
相关文章
|
人工智能 算法 Ruby
|
3月前
|
Ruby
|
3月前
|
Ruby
|
2月前
|
数据采集 Web App开发 数据处理
Ruby网络爬虫教程:从入门到精通下载图片
Ruby网络爬虫教程:从入门到精通下载图片
|
3月前
|
JSON 数据格式 Ruby
|
3月前
|
JSON Ubuntu Linux