Ruby Good news everyone! Ruby 又要添加绿色线程了, Thread::Green (可以理解为 go 的 goroutine)

jjym · 2018年02月04日 · 最后由 jakit 回复于 2018年02月23日 · 9350 次阅读
本帖已被管理员设置为精华贴

https://bugs.ruby-lang.org/issues/13618

翻到了 ruby-lang 的这个 issue,总结下

  1. Eric Wong 给 ruby 增加了可以自动调度的 fiber,暂命名为 Thread::Green。就是类似 go 的 goroutine 这样的轻量级线程
  2. Queue, SizedQueue 等用于同步的类是可以和 Thread::Green 一起使用的。意味着现有的 WebServer 成 Thread::Green 很简单可以迁移
  3. Matz, ko1 等大佬纷纷拍手称赞,(说不定很快就能用上了)
  4. 之后 ruby 可以说摆脱异步编程模型了,直接起 Thread::Green 然后用 blocking IO 就可以和 node 的 callback hell 怼一怼
  5. 对‘应用级别’开发者意味着 Web Server 会更高效,Rails 等框架也会更高效

用法大概就是和 Thread api 会兼容 所以没什么区别,比如下面示例:

urls = ["https://...", ...]
results = urls.map do |url|
  Thread::Green.new(url) do |url|
    # request url
    ....
  end
end

感觉 Eric Wong 这个人很神奇啊,Unicorn 应该也是他写的。很低调、很少出镜,不用非自由软件 (包括 js)..有谁知道他的故事..


坏消息就是我之前写的库已经失去存在意义了 lightio, 不过可以当作个教学示例吧,这些功能从 ruby 层面去实现大概就是这个库

crystal port

hooopo Cool things in Ruby in 2018 提及了此话题。 02月04日 17:20

Auto Fiber 的处理方式非常暴力,Hijack 所有的 I/O 操作,然后直接去把 fd 取出来塞进去,我写的 midori 在处理 redis 连接的也参考了这一做法。Auto Fiber 目前面临最大的一个难点就是 C 语言里面的 I/O 阻塞,因为 I/O non-blocking 的问题是一处阻塞,处处阻塞。

去年 RubyKaigi 我演讲完会后 ko1 问了我怎么看 Auto Fiber,我就讲了这个顾虑。因为 Auto Fiber 的 Hijack 还是基于 Ruby 层的 I/O,如果是 C 扩展,则并不能全自动的转换过去,相应地 C 扩展里需要正确使用 Ruby 新加入的 API,例如 rb_iom_waitfdrb_iom_waitpid 等进行处理。如果 C 程序直接使用了同步方法,或者没有正确使用异步方法,Ruby 并不会主动去调度这些东西。

另外一个难点就是 Mutex。比如 midori 在 monkey patch Redis 驱动的时候,一上来就遇到了死锁问题,这是因为驱动本身并没有考虑到异步后的数据顺序问题。于是额外对里面的锁结构进行了改造,类似的问题在一些 DB 驱动上也是比较常见,甚至 Ruby 一些内置方法的 C 实现也受到影响。

这事实上对于社区压力还是挺大的,现在 Ruby 还是挺想合并 Issue #13618 的,但对于生态的影响也确实有顾虑。不过感觉最倒霉的还是 socketry,iom.h 合并进来,至少把 epoll 和 kqueue 都统一实现了,再不济 nio4r 也没啥意义了。我写的 midori 也有一大半功能都没啥用了。不过长远来看,这也是终于从语言、标准库层面接入异步了,可以说非常进步了,随着 Ruby 3 现在的特性来看,Thread 里包 Guard 里包 Auto Fiber 可以说并发上会很爽。

dsh0416 回复

没细看补丁,从 issue thread 来看似乎他们已经找到解决办法了,在 thread 的很多方法加 hook(假设 C extension 正确的调用这些就可以直接支持),目前应该还是挺乐观的。

Eric Wong: Being in core provides greater compatibility with external libraries which are not aware of existing event loops. So 3rd-party DB adapters (e.g. mysql2) will be able to take advantage of these changes transparently if they use rb_wait_for_single_fd (and I will add a hook for rb_thread_fd_select, too).

jjym 回复

对,方案就是增加这些 hook。但这些 C 扩展还是要手动手动加上 hook 以支持。这几个本质是让同步等待由 Ruby 去监听 fd,等变化了再返回来调用 C。不知道第三方社区的跟进力度会如何了。

dsh0416 回复

这些方法是之前 Thread 也应该用的,所以之前支持稳定优秀的 gem 兼容性已经很好了。

是不是不读个计算机专业就会经常遇到这种听不懂的帖子啊。似懂非懂的,看着好兴奋

jjym 回复

这么说倒是,应该对 Ruby Thread 做过处理的应该都会调用到这些方法了。

huacnlee 将本帖设为了精华贴。 02月05日 16:12

Thread::Green , 意思是之前用的都是红色的?

sevk 回复

Green threads refers to the name of the original Java thread library. The Green Team is the name of the team at Sun Microsystems that designed the Java thread library

https://www.wikiwand.com/en/Green_threads

其实平时用的时候,CPU 占用都是 10% 以下,感觉不出区别。

sevk 回复

和 CPU 关系不大,Thread 语义上是并发的,大致上就是你的程序越多的利用了 Thread,那么并发性能就越好。

但 linux Thread 创建也是有消耗的。而 Green Thread 创建的消耗可以忽略不计,所以程序可以疯狂的使用 Green Thread,最大限度的提升并发性。

你可以用 lightio 打个 Monkey patch, 然后开 10000 个线程试试,在 IO 操作时会比 native 线程有优势 https://github.com/socketry/lightio

听起来就像是当初困扰我的【打算用 C 线程和 Ruby 对象关联,想绕开 GIL 实现并行,但是由于 GC leak 又串不到一起】的问题,有人出来解决了。

另外,GCD - libdispatch 也是类似实现的,效率超高,不过,我輩认为,Ruby MJIT 还没上呢,而且 GIL 没彻底解决之前,没什么好兴奋的。

jakit 回复

C 线程默认就是绕开 GIL 的,反而是你回调 Ruby 线程才需要主动加锁。这个问题,7 年前就加入对应的 C API 了。Feature #4328

MJIT 解决的问题和 AutoFiber(Thread::Green) 解决的不是一个问题,对性能都有很大的影响,但影响的地方不一样。一个是计算,一个是 I/O。

Green Thread 不是 Thread。哪怕在 Ruby 3.0 目前的规划中,跨 Thread 永远是有锁的,Guild 间可以无锁,一个 Thread 可以部署多个 Guild,Guild 间不能共享变量,必须依赖 Guild 通讯。而 Thread::Green 甚至可以是 Guild 之下的,可见和 Thread 没啥关系,更接近于单线程版本的 goroutine。Thread::Green 的加入使得不必使用 Thread 的地方可以不用 Thread,降低上下文切换的成本,这对 Ruby 虚拟机性能影响是很大的。

和 Cocoa 里的 GCD libdispatch 做的也完全不是一个东西。GCD 是对于线程的封装,是来解决并行问题的,而 Thread::Green 是来解决并发问题的,并行不是并发的唯一解决方案,Thread::Green 也不是并行的方案。

jakit 回复

对目前大多数 ruby 程序 (rails...或其他 web server),Green Thread 远比 去 GIL 和 JIT 的作用要大...

dsh0416 回复

我想表达的意思是。

关注点有问题,我刚才说的 绕开 GIL 实现并行,实际上是我用 Autumn 的 select 换成了 epoll 加入到 amber-kit 的实验途中,在对象创建关联上是有问题,经过多番调整,后来是通过了,但是,我发现 epoll 写的 iom 只是 IO 本身高效,但是,实际上你写一个 HTTP SV,或者 FTP SV,你都要 parse 协议的,Ruby parsing 效率很低,好,接着咱们换成 C 来实现了 parsing,这就是我上年回复姜叔叔说我把玩具的 协议 parsing 改成了 C extension 去 parse,当前我未释出的 amber-kit 版本达到了 10k+ req / s。

其实在我做那个试验时候,你的 em-midori 还没加 C HTTP parser 进去,后来我看到你加进去了。

虽然提高了效率,但是实际上问题还在后头,你有业务逻辑代码。

你写这些东西,终究是为了业务服务的,就像你写网站,终究是为了开网站,开网站有内容,有内容的话数据量会越来越大。

经过后面 bench,Ruby obj_a -- call -> obj_b -- call -> obj_c 还有 obj_a new obj_b new obj_c 所消耗的时间,远远比我上面说的写的东西耗时要多了去了。

更何况 Web develop 不像做 OJ 竞赛,没那么垂直和单纯与纯粹,你可以把算法优化到极限不管团队其他人能不能看懂。

GCD 是对于线程的封装没错,而对于 Thread::Green 这个新事物的观点我的评价是【听起来就像是】,这是我感兴趣、疑问的意思,我不太清楚 Ruby 这边发明这玩意来干嘛。

MJIT 解决的问题和 AutoFiber(Thread::Green) 解决的不是一个问题 那跟我说的也不是一回事。

我的观点是,Ruby 与其发明这些 IO 优化的玩意,不如解决计算上优化。

IO 是有瓶颈的,就算你解决了并发,非阻塞,实际上计算性能还是差得老远的情况下,你 Ruby 代码连磁盘 IO 都跑不赢,很可能你刷新一个网页,IO 就那么点东西,但是逻辑就已经够你处理半天了。

我想,Thread::Green 实现的是不用陷入内核态的轻量级调度,解决的是 类似于 Promise,RxJava 那种异步同步的问题吧。这样的话,跟 GCD 也没什么区别,GCD 很大一部分用例就是把线程异步回调同步起来。

jjym 回复

对客户端作用大一些吧,对于服务端做同步这种事情。。。你让服务器去做长时间等待异步同步,那是在消耗服务端资源。

jakit 回复

其实在我做那个试验时候,你的 em-midori 还没加 C HTTP parser 进去,后来我看到你加进去了。

midori 的 HTTP parser 用 C ext 是 2017 年 1 月 6 日随着 commit 3438d00 加入的,你注册 Ruby China 是 2017 年 6 月 10 日。

Ruby parsing 效率很低

快不快慢不慢 profile 一下就知道,midori 换完 parser 对性能影响 < 10%。

我的观点是,Ruby 与其发明这些 IO 优化的玩意,不如解决计算上优化。

IO 是有瓶颈的,就算你解决了并发,非阻塞,实际上计算性能还是差得老远的情况下,你 Ruby 代码连磁盘 I/O 都跑不赢,很可能你刷新一个网页,I/O 就那么点东西,但是逻辑就已经够你处理半天了。

如果计算是瓶颈,那无法解释 Thin 服务器单线程的 Hello World benchmark 为什么能比 Webrick 快将近 20 倍。Web 服务器本来就是 I/O heavy 而不是计算 heavy 的。你用 Ruby 写个堆排那才是真惨,但写完你发现跑的竟然比 Python 3 还快。可见 Ruby 服务器慢绝不是但但的解释器执行效率低这种问题。再反过来 Ruby 上了 RTL-MJIT 我们按最快的那个版本算,就算快 6 倍,比起 Pypy 还是有很大的性能差距,为什么?

实际上问题还在后头,你有业务逻辑代码。

网站的业务逻辑绝不是性能瓶颈,大多数 API 核心就是 CRUD,CRUD 的部分的计算都是依赖数据库在运行。自然很大的问题是等待数据库时的性能,而不是 Ruby 本身运算的性能。

IO 就那么点东西,但是逻辑就已经够你处理半天了。

究竟 I/O 是大头,还是逻辑是大头,不是想当然说出来的。是通过跑 profiler,跑 benchmark,跑 gdb lldb 找出来的。高德纳说过:「程序员浪费了大量时间考虑程序非关键部分的速度,但如果算上调试和维护,这些性能企图事实上带来严重负面影响。我们必须忘记微小性能收益,97% 的时候:不成熟的优化是万恶之源。但也别放过优化关键 3% 的机会。」优化性能的关键就是去找这 3%。如果为了计算上的极致优势,那我们都应该写汇编。

Thread::Green 实现的是不用陷入内核态的轻量级调度,解决的是 类似于 Promise,RxJava 那种异步同步的问题吧。这样的话,跟 GCD 也没什么区别,GCD 很大一部分用例就是把线程异步回调同步起来。

无论 Promise,RxJava 还是 Thread::Green 和 GCD 解决的问题都有很大的区别。这里面只有 GCD 是「把线程异步回调同步起来」,剩下的都不是。如果为了解决 I/O 异步去用 GCD,那 performance 可以说是惨不忍睹的。GCD 是来做并行计算的同步回调的,而不是解决并发 I/O 的同步回调的。这是完全两个概念。比如你用 GCD 写个 fibonacci 就会有性能提升,剩下几个写 fibonacci 都不会提升。但写个 RPC 则是前几者的性能提升很明显,后者提升不明显。这两个东西不但完全解决的是不同问题,而且从原理和实现上也差的非常多。

不能因为名字里都有「异步」「回调」「同步」就认为是一个东西。既然你那么喜欢 Cocoa 的 API。那么关于 Web Server 用 GCD 本质是 blocking 的这个问题 IBM Kitura 2 年前就讨论过了。https://github.com/IBM-Swift/Kitura/issues/121。或者看 vapor 去年 11 月前后的 GitHub Issue,社区试图从 libdispatch 转换到 non-blocking I/O 所作的工作。

对客户端作用大一些吧,对于服务端做同步这种事情。。。你让服务器去做长时间等待异步同步,那是在消耗服务端资源。

你不让服务器等待异步同步,就要等待阻塞同步,或者线程上下文切换。你可以比较一下到底哪个消耗资源。

dsh0416 回复

首先,服务器都是开辟/派遣一个 thread 你的一次 action 的,要不就是 process,抢占任务都一个样,等待数据库那是远程调用的事,而且 MySQL 的组件是 C Extension。

如果不是 Ruby parse 数据包(无论 HTTP 还是 ws 还是别的)太慢你会换成 C 来 parse,Ruby parse 数据包带来的延迟的速度比跟客户端 IO 影响得还要高。

另外,服务器的 Ruby IO 的问题个人认为是 Ruby String 类 效率问题,你从流读取会把 buffer 读入里面,但是换做 C char 作 buffer cpy 处理,效率完全不一样。

读了一下 Thread::Green 的代码,其实 FD 通信,本身就是 blocking 的。。。比如 select 就是个阻塞调用,阻塞的地方在这个调用本身。

GCD 本质确实是 blocking 的没毛病

RPC 不要拿出来说,分布式就有一个 RPC 一粒粒组件之间调用带来的延时问题。

jakit 回复

服务器都是开辟/派遣一个 thread 你的一次 action 的

谁告诉你的?你要不看看 Node.js 这种连自带原生线程这种东西都不存在大几千 rps 的,或者 nginx 这种单线程下也可以几十万 rps 的,是怎么处理 action 这个问题好不好?这两个都「开辟/派遣」thread 了吗?没有,开销那么大的事情讲究性能谁会去做?

你倒是反过来理解一下,对于 CPU,几个核就能同时做几件事,Thread 在操作系统层面的本质到底是个什么东西。对于网卡硬件,一次只能读进一个二进制流,多路的本质是什么?讲不清这两个问题你才会陷入对多路复用的理解仅限于 Thread 的解决方案。

读了一下 Thread::Green 的代码,其实 FD 通信,本身就是 blocking 的。

求求你把 iom.h 重新读一遍,谢谢。

比如 select 就是个阻塞调用

。。。一个 multiplexing 的东西要是变成阻塞调用了,怕不是各个操作系统内核都要气死。select, epoll_wait, kevent 本质上做的是一个事情,而他们实现方法不同,导致了效率的区别。你在 Thread::Green 里看到的 select 也不是真正的 select,而是在 iom.h 里对 select epoll 和 kqueue 进行统一封装后的 API。

服务器的 Ruby I/O 的问题个人认为是 Ruby String 类 效率问题

你随便怎么 profile,能 profile 出这个结果也是算你牛逼的。

dsh0416 回复

我不管怎么封装,反正都一个目的,都是把事件异步的系统调用,what about iocp with Win64?

你在 Thread::Green 里看到的 select 也不是真正的 select,而是在 iom.h 里对 select epoll 和 kqueue 进行统一封装后的 API。

这是废话,GCD 的 libdispatch 也说自己是 kqueue 封装后的 API……

其实我很好奇的是,我看你们研究不同的 IO lib 跟玩玩具一样的。。。为什么不直接用系统 API?

jakit 回复

其实我很好奇的是,我看你们研究不同的 IO lib 跟玩玩具一样的。。。为什么不直接用系统 API?

你能正确封装各个操作系统 API 一起用那也算是大几千个 Star 的项目了,操作系统级别的 API 哪里那么简单。从 libev libevent 到什么 Java 里的 NIO 或者 Rust 里的 tokio 做的都是这件事。你以为看着文档写个 kqueue 就能用了?不讲别的,你先搞清 kqueue 在 Darwin 内核里和 OpenBSD 内核里有哪些区别,MacOS 和 iOS 的各个不同版本 kqueue 的行为是否一致就可以搞好几天了。随随便便就说调用操作系统 API 才叫玩玩具。

像 libevent 里对于 macOS kqueue 上的 bug 从 16 年讨论到 18 年了,有好几个其他 lib 都是假设 mac 不会被用作服务器而忽略这个 bug 的。再比如 midori 中对于 macOS 还有一个 patch

class TCPServer
  def tcp_fast_open
    opt = (/darwin/ =~ RUBY_PLATFORM) ? 1 : 5
    self.setsockopt(6, Socket::TCP_FASTOPEN, opt)
  end
end

一个调用在 Linux 和 BSD 下是什么样的。BSD 是这样,fork 自 BSD 的 Darwin 是不是还这样?这些都是要非常小心测试的。不是写了能运行就代表能用。也不是我能用,大家都能用这么粗暴的关系。

软件是一个系统工程,很多问题,特别是对于缺乏出问题经验的人会想当然被认为简单。只有出过几次问题,最好这几次问题赔了很多钱,自己才会开始谨慎。

我就说写个 Hello World 好了,你看看是不是在 docker 的 log 里你的 Hello World 程序不 return 掉,log 都不会正确打印?你这时候要返回去看 printf 到底干了啥。STDOUT 是啥?怎么还操作 I/O 了?怎么还有 buffer 了?buffer 在什么情况下会 flush?docker 的 STDOUT buffer 长啥样?一圈看下来才发现,原来写个 Hello World 也能出问题,还得回去看标准库和操作系统的实现。

软件就是这么复杂,不是想当然的想用什么用什么,想写什么写什么的。

dsh0416 回复

你说的情况,我也是在 Darwin 碰到过。。。不过不是 IO API。而是 inet 部分的,这个很正常,Linux 内核本身就不是完美的,要不然 ubuntu 就不会有 LTS 长期维护保证持续 fix,macOS 其实升级也会更新 command line 还有 XNU。

遇到这种问题,每次都找 Core Team report。嘛,嘛,自己 fix 不太现实。

使用 lib 确实好解决平台问题,比如 boost::asio。自己写是遇见这种问题的。

jakit 回复

你先搞清楚 LTS 到底是在 fix 什么东西。。。。或者说所谓的发行版的维护策略到底是在做什么

jasl 回复

你不知道 lts 系列是有软件包升级吗?内核会有更新

jakit 回复

你不知道搞 lts 的目的就是要保持软件包的行为一致,解决硬件适配和安全问题吗?

jakit 回复

...你需要先搞清 LTS、rolling distribution 和 half rolling 各自都在干什么,以及为什么需要这么干。LTS 是不会也不能引发 Kernel 的 API 变化的,如果引发了,那么这个发行版基本就完蛋了。

dsh0416 回复

话题绕太远

回到原来的话题,实际上我本来想表达的就是 Ruby 支持并发,但不并行。

在 GIL 场景下,你除了写扩展,或者出门左拐 JRuby,在 MRI 传统 rb 编码是分时并发的,它解决的是均衡让多个线程能等分 CPU 时间,实现了一个虚假的“并行”,但实际上只要 GIL 的存在,它消耗的时间是不会少的。

但是由于 IO 能绕开 GIL,IO 是不会被 GIL block 的,那是系统层面等待而不是语言平台级别去等待。所以我看大家都在极力证明服务器 IO 密集啦什么的。

但是我个人观点是对于业务复杂的 site,基本都是 programming complex,造成 computing complex 和 CPU bound,A 类 -> B 类 的一大波业务,还有字符串处理等等,不是小损耗。而系统 IO 基本都是 C 甚至 Assembly Instruction(汇编指令)来完成,实际上提到的那些 Goroutine java.nio,都是为了把【等待】给异步开来,不阻塞原本 Main Thread 的 code 执行。

研究 IO 的本质就是【模型】,玩来玩去就是设计模式,所以我才会说“玩玩具一样的,还不如直接调用系统 API”,虽然细看会有差异,还可能调度上的差别产生了性能差别。

但是,我提及 asio,是我觉得研究这个还不如换 C / C++ / Java 去 implement,因为 Ruby 似乎并不擅长这个,就算写出来了,一个方法调用就已经内部损耗了很多重、一大波的指令。

所以我才会描述很极端,但又很现实的情况:

你 Ruby 代码连磁盘 IO 都跑不赢,很可能你刷新一个网页,IO 就那么点东西,但是逻辑就已经够你处理半天了。

其实 Ruby 简单的异步 IO API 就已经完胜应对 IO 了,puma 能做到如此精简同时高性能。

与其浪费时间去折磨这些,不如转 C++ / Go / Java,如果深爱 Ruby,不如好好思考一下 GIL 下一步该怎么解决。

jakit 回复

你写过多大规模的 site... 说这么多不妨直接上点数据来

jasl 回复

我写的 cocos - tars 游戏服务项目算是吧,不过日志比玩家信息数据要多,差不多吧,咿呀哈哈哈哈

其中的一小小部分,其实还有分布式好几个 Server 的 log

不过用 Cpp 写的,跟 Ruby 无关,在这里发言会不会让众多 Rubyist 有看法

jakit 回复

你这也不是典型 web 负载啊,而且我说你可以上数据你回的这叫什么?

jasl 回复

好啦,逃 👣

需要 登录 后方可回复, 如果你还没有账号请 注册新账号