Ruby [讨论] EventMachine 和 Fiber

zw963 · 2013年08月23日 · 最后由 zw963 回复于 2013年08月26日 · 8253 次阅读

好吧, 说的标题足够大, 但其实只是期望讨论我在网上看到的一段代码, 并藉由这段代码, 来推导 (好吧, 我天生是个推理狂) 出来, EM 和 Fiber 是如何一起工作的.

原始代码来自于: http://www.igvita.com/2009/05/13/fibers-cooperative-scheduling-in-ruby/ , 我的代码是对这个代码的修改版, 只为了搞清楚, 整个代码被执行的流程是如何进行的.

我就不在这里直接贴了. gist 地址如下, 各位可以拷贝到自己习惯的编辑器中再阅读. 想要正确被执行, 记得首先安装两个 gem. eventmachine em-http-request

https://gist.github.com/zw963/6319792

其中有关 Fiber 的介绍是给不懂 Ruby Fiber 的朋友看的, 懂的可以忽略, 至于后面的流程分析, 是我花了一下午时间的 推理结果, 还不一定全对. :) 请对 EM 了解的朋友帮我看看, 是否有明显的理解错误, 欢迎指正出来.

有关 EM 如何实现 Reactor 的资料貌似不多, 谁有不错的资源, 尤其是 EM Reactor 模型以及线程的关系方面的资料, 请贴出来. 先谢啦.

好吧, 我自作主张, 先密几个我知道的几个可能对 EM 很熟悉的 id. @flyerhzm, @luikore, @yedingding.

希望所有感兴趣的朋友, 都对 EM 多给宝贵意见.

感觉用起来还是太坑了,理论上,Twisted 和 Stackless 也是可以一起工作的,但是抢占式调度该怎么搞,感觉我的智商只够用用 Erlang

#1 楼 @bhuztez 请教下抢占式到底是啥,总是听你们在说

#3 楼 @jjym 就是你的程序运行了很久很久还没运行完,我怒了,把你的程序暂停,让我的程序先执行

#4 楼 @bhuztez ....那抢占式调度呢...?

#1 楼 @bhuztez

抢占式调度有两种意思. 一种意思是多个进程抢着 accept 同一个 file descriptor , 和 event loop 可以完全协同工作啊, nyara 的 production server 就是这样整的. 另一种意思是人为的在字节码/机器码中隔三岔五的塞进类似于 yield point 的调度点 (例如 JVM 里有几十种迷你锁, 锁和锁之间都是可调度的), 细到一定程度就出现了抢占的效果, 多用于支持线程的虚拟机中, 塞太多对于 web server 没什么好处, 反而增加了 context switch 的开销... 塞少一点或者只塞到 EWOULDBLOCK 的时候, 就和 event 类框架差不多了, 再把控制权交给 OS, 就变成 event loop 了...

stackless 迷你线程的做法挺好的啊, 不过为什么很多人都转用 gevent 了?

#6 楼 @luikore 也许因为 CPython 官方维护者无力把 Stackless 的代码合并到主干

Problems 列了很多 http://www.python.org/dev/peps/pep-0219/

#6 楼 @luikore 你那两个都是 cooperative,cooperative === 没调度。抢占式调度,主要的两种实现,一个是 OS,直接定时器中断,另外是解释器 VM,数执行指令个数,超过一定个数就不让你执行。

http://www.stackless.com/wiki/Scheduling

#8 楼 @bhuztez 你说的这两种和我说的 隔三岔五塞调度点 难道不是一个意思...

#9 楼 @luikore 因为要抢资源的还没有抢到而放弃执行机会去排队抢资源的不算 preemptive,比如锁

你的例如歪了

事件框架的问题 1 是, 破坏了对象的封装...

本来大家都是有主观能动性的, obj.write 现在变成了 obj.send_data, 只是改了个名字本质没什么区别, obj.read 却变成了被动读 def obj.receive_data. 两个对象只要相互收发数据, 就得参照对方的实现去写.

问题 2 是, 数据流不是自然按照写代码的顺序呈现出来, 而是通过手动拼接回调接起来的.

这里不得不提下 Haskell ... Haskell 有专门的语法糖把这种回调套回调变成一个和顺序写法很相似的东西. 例如一个 do-notation

f x = do
  a <- g x
  b <- h a
  ...

相当于翻译成用 >>= 串起来的样子

f x = return x >>= \x -> return (g x) >>= \a -> return (h a) >>= ...

>>= 虽然是个左结合的二元运算符, 但坑爹的地方是 lambda 语法是能往右边延伸多长就多长的, 其实 do-notation 每一行都是回调有木有...

f x = return x >>= (\x ->
  return (g x) >>= (\a ->
    return (h a) >>= ...
  )
)

所以实际上这个丑陋的回调套回调, 通过语法糖变成了优美的 do-notation...

不过 Fiber 走的是另一条路... 线程的特点是每线程都有自己的栈, 读写共享数据的时候需要锁. 而环保线程/Fiber 是 cooperative 的调度, 自己在跑的时候就是老大, 不用担心什么时候被人抢占, 所以不用加锁, 不用做全栈拷贝, 只用在建立出分个叉就可以了. Fiber 的调度权在于自己 (Fiber.yield), 而不是 VM 或者计时器插一脚进来的.

Fiber 的用法举例:

f = Fiber.new{ i = 0; loop{ Fiber.yield i += 1 }}
f.resume #=> 1
f.resume #=> 2

既然 Fiber 可以暂停和重启, 那么我们可以利用这个特性把 def obj.receive_data 改造成像 obj.read 一般人性友好的 API.

我们可以这么实现 read:

def read
  while not eof
    read_nonblock # 当然在 EM 里你就要对 receive_data 做点很搅脑的操作了...
    Fiber.yield
  end
end

client 对应的 EM::Connection 可以用 Fiber 包起来, 当捕捉到事件的时候, 就调用 fiber.resume 去继续操作. 于是就实现了 em-synchrony.

#11 楼 @luikore 那个和 Twisted 的 calllater 一样,就是根据下个定时事件发生时间减去当前时间给 select/poll 设了个 timeout。但是,这个前提是全都是 cooperative 的

Fiber 的调度权在于自己 (Fiber.yield), 而不是 VM 或者计时器插一脚进来的.

所以这个才是问题。我该什么时候打断他们,去调用 select/poll,timeout 又应该设置为多少?

#13 楼 @bhuztez EM 中 epoll 好像不要自己调

fiber = Fiber.current
EM.next_tick {fiber.resume}
Fiber.yield

这就相当于 thread yield 了吧

好吧我错了。。貌似 fiber 只能在调用 yield 的那个线程 resume

虽然要使用者自己写 yield,且只能用单核,但是是最容易实现的并发方式了吧。 golang 也用了很久的纯协作式的调度器,但不需要写 yield,也能用上多核。最近又给调度器加入了抢占的动作,来解决协作式调度里进程占用过长的问题。所以说抢占和协同不是水火不容的东西。

@bhuztez 在 read/write 碰到 EWOULDBLOCK 的时候插 yield 进去, 就不用手动写 yield 了

马克一下两个链接: http://golang.org/src/pkg/runtime/proc.c http://state-threads.sourceforge.net/docs/st.html

@reus goroutine 里大概也是在 read/write 里用长跳转把控制权交回调度器, 相当于 yield 了吧?

#17 楼 @luikore 碰到 read/write 的时候,难道不是直接加到 select/poll 里去?

#17 楼 @luikore 在读写 chan、分配内存、进出系统调用时会调度,以前是这样。现在是需要分配 split stack segment 时也可能被抢掉 CPU。 https://groups.google.com/forum/#! topic/golang-dev/vtrWpvf8nMA golang.org 上的代码是发布版本的,比较老了,抢占调度在开发版本里才有,看 google code 的 repo 里的吧 有人在写研究实现的书,可以参考下 https://github.com/tiancaiamao/go-internals/blob/master/ebook/preface.md

周末一直没有空花费整段的时间, 来细细品读诸位大神的讲解, 现在终于有时间了.

#1 楼 @bhuztez 忘了 B 大也精通, 抱歉呀抱歉, 要是个人对 EM 了解太浅了, 甚至没有跟 erlang 联系到一起, 所以下次一定记得诸如此类问题, 捎带密下你.

#11 楼 @luikore 我表示看得晕晕乎乎.

我想先分享下有关我对于 什么是 socket , 什么是 Server 以及如何建立一个 connection 的理解,针对和我一样读起来晕晕乎乎的社友, 部分内容来自于百度百科中的解释 (对于自己比较陌生的东西, 先看看百度百科还是不错滴), 不一定对, 欢迎指正:

  • 什么是 socket. 当进程通信之前, 双方必须各自建立一个端点, 否则是无法建立连接并进行通信的. socket 就是进程通信的端点.socket 是为 C/S 模型设计的, Client 会随机申请一个 socket, Server 端则拥有全局公认的 socket. 除此之外, 双方的 socket 没有任何区别.应用程序通常通过 socket 向网络发出请求或者应答网络请求。

  • 什么是 Server ?? 我觉得应该包含两部分:

    • 产生一个 `阻塞', 这是一个术语, 表示暂停在某个地方, 直到一个会话产生, 然后程序继续. 举例说明: 我们首先会使用 server = TCPServer.new(2000), 建立一个 TCPServer socket. 然后我们会调用 server.accept, 暂停代码的执行, 等待 Client socket 的连入 (即: 所谓的监听) 一旦监听到 Client 的请求, 执行三次握手的后两次握手 (发送服务器信息, 并等待 Client 确认信息) 之后, Server 端会创建一个新的 TCPSocket, 和 Client 的 TCPSocket 建立连接. (更通用的情形是: 在一个新的线程中, 执行后两次握手, 并建立新的连接.)
    • 应该存在一个无限 loop, 只有这样, 在前一个连接结束之后/新线程建立一个连接之后, 继续通过 server.accept 监听.
  • 还有一个有关什么叫做 epoll 的转贴 http://yaocoder.blog.51cto.com/2668309/888374

好吧, 我还是自己先看懂再说...

@luikore

我表示..., 终于将 EM 和 设计模式中的观察器模式 还有 Linux 下的 libevent 关联到一起了.

@luikore , 那么 EM 采用的事件驱动, 到底有没有用到 Linux 内核提供的 libevent 机制?

@luikore , 可能我提出的问题有些问题, 也许应该这样问:

linux 下的 libevent 库 (典型的, 我指的是侦测当前目录下的文件改动, 或通过 notify-send 发送一个通知), 跟 Linux 内核提供的 epoll 是否有关?

#22 楼 @zw963 不, libevent 也是和 EM 类似的东西, 是事件的跨平台实现. 然后它们的实现里才是针对各系统不同而使用了 epoll / kqueue / iocp, 系统不支持的话就退回 busy loop 或者加点 sleep 的 busy loop.

用 win32 API 写窗体程序的话, 也有这么个 loop

while GetMessage(msg, ..) { // 没事件就 sleep 了
  TranslateMessage(msg);    // 把输入设备事件转换成 UI 事件
  DispatchMessage(msg);    // 调用注册好的 wndproc
}

#24 楼 @luikore

已经清楚啦, 哈哈.

sqsy 整理学习 EventMachine 的一些文章和帖子 提及了此话题。 09月08日 17:05
需要 登录 后方可回复, 如果你还没有账号请 注册新账号