好吧,说的标题足够大,但其实只是期望讨论我在网上看到的一段代码,并藉由这段代码,来推导 (好吧,我天生是个推理狂) 出来,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 多给宝贵意见。
抢占式调度有两种意思。一种意思是多个进程抢着 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/
# 例如歪了
我来说明下 EM 的原理吧
一个 blocking server 是长这个样子的:
# blocking loop, 没事干的时候会安静的 sleep
loop {
client = server.accept # 接受请求, 可以 ri accept 看看它的功效.
client.read # 其实 ruby 做了点处理, 和系统函数 read(blocking_fd) 不同, 只会塞住本线程
client.write
...
}
一个 nonblocking server 是长这样子的:
# busy loop, 没事干的时候还会吃 CPU 100%
loop {
# 把存着的 client 都跑一趟
client = server.try_accept # 伪码... 不过 kgio 里是有个 try_accept 的, 反正就是不塞住
client.read_nonblock # 不塞住, 所以就不一定成功...
client.write_nonblock
...
# 没跑完? 把 client 存起来
}
(以 linux 为例) 一个 event server 是长这样子的:
# event loop, 没事干的时候也很安静
loop {
event = epoll(timeout) # 当然也可以 poll 多个, 但这里就简简单单的 poll 一个
if event means server readable
client = server.accept
...
end
}
系统事件包括 fd (文件描述符) 可以读 (EPOLLIN), fd 可以写 (EPOLLOUT), 定时器跳跳,文件系统被改,信号...
系统提供了:
epoll_create
epoll_ctl
epoll
最后你写的服务器就变成 loop { epoll ; ...}
的样子
EM 做的事情,就是把上面 loop 中的 client 包装成 EM::Connection
对象,然后碰到 "fd 可读" 事件时,回调 Connection
对象的 receive_data
方法。
所以 EM.start 是长这样子的:
# 各种初始化后...
loop do
event = epoll(timeout)
conn = find or create connection by event
conn.receive_data(read_nonblock)
schedule thread # 触发线程调度和信号处理
end
multiplexing 没问题了,读也是非阻塞了,那写数据 send_data
是怎么处理的呢?
至此,EM 就产生了两个问题:
close_connection_after_writing
EM.next_tick{ ... }
还有一点,EM::Connection
对象除了来源于 server accept 以外,还可以来自 socket connect
事件框架的问题 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.
虽然要使用者自己写 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 在读写 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 联系到一起,所以下次一定记得诸如此类问题,捎带密下你。
我想先分享下有关我对于 什么是 socket
, 什么是 Server 以及如何建立一个 connection
的理解,针对和我一样读起来晕晕乎乎的社友,部分内容来自于百度百科中的解释 (对于自己比较陌生的东西,先看看百度百科还是不错滴), 不一定对,欢迎指正:
什么是 socket. 当进程通信之前,双方必须各自建立一个端点,否则是无法建立连接并进行通信的。socket 就是进程通信的端点.socket 是为 C/S 模型设计的,Client 会随机申请一个 socket, Server 端则拥有全局公认的 socket. 除此之外,双方的 socket 没有任何区别。应用程序通常通过 socket 向网络发出请求或者应答网络请求。
什么是 Server ?? 我觉得应该包含两部分:
还有一个有关什么叫做 epoll 的转贴 http://yaocoder.blog.51cto.com/2668309/888374
好吧,我还是自己先看懂再说...
@luikore , 可能我提出的问题有些问题,也许应该这样问:
linux 下的 libevent 库 (典型的,我指的是侦测当前目录下的文件改动,或通过 notify-send 发送一个通知), 跟 Linux 内核提供的 epoll 是否有关?