Rails 写后端非常爽,但是写前端就很痛苦,于是拉上体验更好的 Next.js ,直接有完善的组件、IDE 和社区支持,组合成 Next.js 代理 API 请求到 Rails 的架构。
演讲者还提到一个好处,就是 Rails 基于 cookie 的 session 管理非常完善,通过这个架构就可以重用这部分,而不需要搞什么 JWT token 。但是从演讲里看,Next.js 和 Rails 之间交互也没特别方便,例如 Rails 要关掉 CSRF token 之类的东西,这反而可能被利用。
这类涉及前后端开发的问题,肯定又扯出前后端分离、团队搭建的问题。我自己很久没写前端了,所有是偏向于分开的,但是很多 Rails KOL 都喜欢全栈的感觉。
这是我挺期待的一个主题,我还做了功课,去看了 Fiber scheduler, evt, async ,测试了用 falcon 跑 Rails 的性能。
演讲者引入异步的原因是因为 LLM API 响应慢,所有如果不异步,那发个 API 请求就要阻塞一个线程,所以异步势在必行,举的例子也主要是用 Fiber 做异步 API 请求,但没细讲(或者本来就不值一讲吧)。演讲者称 LLM 带来了编程范式的转换,其中包括从 CPU 密集型变成了 IO 延迟式,这就有点强行了。后面就是简单的 Fiber 介绍,和 async/await, promise, goroutine 的对比,过于简单,就列表格讲个概念。
我是挺失望的。标题号称十倍性能提升,我希望是有数据支撑的,但是没有。对机制的思考,我以为会讲 Fiber 和 Fiber scheduler 的原理,但这些都简单带过了。而应用也只有 API 请求异步化(还很太快翻过的 concurrency gem 例子,看不清啊)。
就当抛砖引玉吧,最后呼吁大家来共建 Ruby 的 LLM 和异步生态。
一个新的 stack profiler ,在 ruby 进程内通过一个独立线程扫描 ruby 的调用栈,但不获取 GVL 所以基本不影响应用性能。有 data race 的风险,但一点错误不影响性能分析的目标。函数地址和文件名/函数名的关系是通过 ebpf 的 uprobe/uretprobe 记录的,只在方法被定义的时候会记录,不用重复解析。
这个演讲的准备就非常好,提出问题,分析现有方案的缺点,提出改进设计和目标,通过性能对比证明设计的可行性。
这里的 monolith ,是指前后端放一起,所以和第一个主题是类似的,就是为了更好地在 Rails 项目里写前端代码。
通过一个 Todo list 的应用,分别演示 Hotwire, ViewComponent, Inertia.js 的方案。作为 demo 我觉得挺失败的,没有真实演示,只有截图,视图代码也复杂,属于不懂的完全就跟不上的类型,大部分只是浪费时间,还好 Inertia.js 是讲清楚原理的。
最后也有观众问了 monolith 的定义问题,在一个 Raisl 应用里有多个领域的(没拆成微服务),但做了前后端分离还算不算 monolith 。演讲者说是你应用业务逻辑膨胀,这都不能算是技术问题了(大意如此)。
非常“小众“的主题,演讲者也做了很多准备,PPT 塞了很多内容,,Metasploit 历史,架构,环境,TruffleRuby 什么都有。但是演讲效果不太好,语速飞快,我只能听个大概,感觉内容都是蜻蜓点水,结果就是什么都没学到。
Demo 效果非常好,也分享了很多工程上的实践,如分层的架构( ASR + TTS, turn-taking, LLM),状态的管理,流式性能优化,prompt ,tool calling ,对生成内容的优化……具体还是看回放吧。
激起了我对 LLM 开发的兴趣。
而且看代码其实是 Go 实现的,没有 API 阻塞线程的问题。状态机(演讲者把它叫 Routine )的实现里,每各状态是一个方法,每 50ms 执行一次,接受参数(例如 LLM 的输出)做逻辑判断,通过返回下一个 Routine 实现状态迁移。有手写 coroutine 的感觉。
用 AI 生成游戏剧本,demo 也很有趣,但是为了做出这个效果做了好多工作,都不止是 LLM 的范畴的。
例如实现了一个 DSL (最后是 Blockly 的形式)来写剧本。用 Logica 来验证 Storylet 的逻辑正确性,还能讲一阶逻辑,二阶逻辑。让我意外的是 Logica 生成 SQL 给 PG 查询居然比 Prolog 更快,原因之一是减少了加载数据到 Prolog 的过程,Prolog 有没有类似的 image 或者数据库的持久化功能?
还有白学:)
最后观众讨论的是生成剧情的游戏可能遇到的问题,而且真的有市场吗?
]]>由于一直对 Ruby 很感兴趣,上面的书基本上都看了一遍以上,感觉学到了很多知识,但却没有做出什么应用 (主要还是受限于想法、设计等能力),于是想着不妨把学到的知识总结出来,一方面是为了自己巩固,另一方面也希望能对 Ruby 感兴趣的新人有所帮助,所以创建了一个 Ruby 技术公众号 “Ruby 札记”,发了第一篇文章 Rails 如何处理静态资源。
在 Ruby China 也发了一篇帖子.
]]>MRuby Devkit 是一个开箱即用的脚手架。 基于 MRuby 将你的 Ruby 代码打包成 二进制可执行文件。
方便开发类似于 Golang 的二进制可执行文件。
—— 灵感来自于 Golang 可以编译为二进制可执行文件的迷人特性。
接受简单的约定,专注于编写 Ruby 代码。轻松地 build 成二进制可执行文件。
src
下编写 ruby 程序
模仿 golang 的 go run
rake run
模仿 golang 的 go build
rake build
借助 Github Action 编译不同平台的可执行二进制文件。
大家好,我是 Mark24 。
分享下我的笔记,使用 Ruby-build 在 MacOS 上 编译 Portable ruby
设想一下,如果 ruby 可以变成 portable 的,放在 U 盘上就可以带走,传输到任何一台电脑上就可以执行。
Portable Ruby + 你的 Ruby 代码 的 zip 包,就像一个行走的独立软件。就像 Go 打包的一样。
你还可以把他们塞入 一些壳软件里。就像 Electron 那样运行(内部是个浏览器)。
当然 Ruby 社区曾经有很多方案 Traveling Ruby 、Ruby Packer , 都用各自的方式实现类似的效果,不过都不维护了。
下面用一个简单的方法来制作 Portable Ruby 。
截止 2024-05-27 最新版本是 3.3.1 。 每个版本因为特性的不同构建是一个动态的过程。就以 3.3.1 为例。
过程偷懒,建立在 ruby-build( https://github.com/rbenv/ruby-build) 的基础上。
不论是 asdf 、rvm …… 他们的背后都是 ruby-build 一个方便安装的 standalone 的工具。ruby-build 解决了大部分的问题,我们只需要找到合适的构建参数。
1.安装 Mac 的基础工具集
终端输入 xcode-select --install
2.安装上 homebrew
获得 类似于 Linux 上的包管理工具
3.安装 Ruby 编译需要的前置依赖
# 安装前置依赖 # ruby-build 是安装工具 # openssl@3 readline libyaml gmp 是必要的依赖 # rust 是 YJIT 必要的依赖,不装就不会构建 YJIT 功能 brew install ruby-build openssl@3 readline libyaml gmp rust
0.知识点
C 语言( CRuby 是 C 语言项目)编译一般分为 3 个基本过程 1 )预处理:处理一些前置的宏替换 2 )编译:把 .c 代码文件翻译成 .o 机器码文件目标文件 3 )链接:把 .o 文件和系统的底层库(比如标准输入输出)正确的关联起来。生成可执行文件 链接这部,有两个基本的实现 1 )静态链接 2 )动态链接 静态链接比较简单,就是把所有用到的代码打包成一个整体。软件就像一个 exe 文件,带到哪儿都可以执行。 优点就是,随处执行。缺点就是体积大,更新困难,比如你依赖的系统部分有安全缺陷。你必须整体替换。 动态链接,就是软件把用到公共部分(系统、上游 lib )的部分,指他们的动态库( linux 是 so 文件,windows 是 dll 文件,mac 里是 dylib 文件)。 优点:体积小, 如果公共部分有安全漏洞,系统更新,只需要更新动态链接库文件,所有引用的软件都会获得更新。 缺点:除了无法 portable ,软件运行的前提是系统拥有相应的 库。 动态链接是常态,不论是 Linux 、MacOS 、Windows 。动态链接的实践这么多年运行的一直很好。通常库都是按照动态链接库方向来设计的。没有提供静态库。 MacOS 还禁止系统动态库进行 静态链接。
关键参数:
$HOME/portable-ruby
是你存放的目录--enable-load-relative
地址是相对目录,这对我们移动很重要--with-static-linked-ext
静态链接RUBY_CONFIGURE_OPTS="--enable-load-relative --with-static-linked-ext" ruby-build 3.2.2 $HOME/portable-ruby
2.一些优化选项
可以参考 https://github.com/rbenv/ruby-build
额外的选项
--with-out-ext=win32,win32ole
去掉 MacOS 上不需要的拓展--disable-install-doc
关闭文档,减小体积--disable-install-rdoc
--disable-dependency-tracking
RUBY_CONFIGURE_OPTS="--enable-load-relative --with-static-linked-ext --with-out-ext=win32,win32ole --disable-install-doc --disable-install-rdoc --disable-dependency-tracking " ruby-build 3.2.2 $HOME/portable-ruby
ruby-build 能做的更多,比如支持交叉编译
编译正确完成,你应该获得了 portable ruby
在拥有 依赖库的电脑上(对,我们前面解释了,系统部分是禁止 静态链接的)。
你的可以把你的 ruby 代码 + portable ruby 放在一个文件夹里。 用 一个 shell 脚本,通过相对路径连接起来执行。
比如这样
#!/usr/bin/env bash ./portable-ruby/bin/ruby ./main.rb
某种意义上,Portable Ruby + Ruby Script 和 Go 、Crystal 打包的可执行文件,是一样的。就是大了一点 :D
]]>Ruby 已经有了异步实现!
它现在就可使用,已经做好了投入生产的准备,而且它可能是过去十年甚至更久时间里 Ruby 发生的最令人振奋的事情。
Async Ruby 给这门语言添加了新的并发特性;你可以将其视为“没有任何缺点的线程”。它已经在酝酿了几年,也终 于在 Ruby 3.0 中准备好进入主流。
在这篇文章中,我希望向你展示异步 Ruby 的所有力量、可扩展性和魔力。如果你热爱 Ruby ,那这应该会让你非常激动!
什么是 Async Ruby ?
首先,Async 只是一个 gem,可以通过 gem install async
进行安装。这是一个相当特殊的 gem ,因为 Matz( Ruby 的创始人) 请它加入 Ruby 的标准库,但邀请还未被接受。
Async Ruby 是由 Samuel Williams 创建的,他也是一个 Ruby 核心贡献者。Samuel 还实现了 Fiber Scheduler (纤程调度器),这是 Ruby 3.0 的一个重要特性。它是"库无关的",未来可能有其他用途,但目前,纤程调度器的主要目的是使 async gem 与 Ruby 无缝集成。
并不是很多 gem 能得到他们自定义的 Ruby 集成,但这个是值得的!
所有这些都告诉你,async
不是"只是外面的另一个 gem"。Ruby 核心团队,包括 Matz 本人,都在支持这个 gem ,希望它能成功。
Async 还是一个 gem 生态系统,这些 gem 能很好地一起工作。以下是一些最有用的例子:
async-http
是一个功能丰富的 HTTP 客户端falcon
是围绕 Async 核心构建的 HTTP 服务器async-await
是 Async 的语法糖async-redis
是 Redis 客户端虽然上面列出的每一个 gem 都提供了一些有用的东西,但事实是你只需要核心 async gem 就可以获取它的大部分好处。
Asynchronous programming (异步编程),(在任何语言中,包括 Ruby )允许同时运行许多事情。这最常见的是多个网络 I/O 操作(如 HTTP 请求),因为在这方面 async
是最有效的。
多任务操作经常带来混乱:“回调地狱( callback hell )”,“Promise hell ( Promise 地狱)”,乃至 "async-await hell ( async-await 地狱)" 是其他语言中 async
接口的众所周知的缺点。
但 Ruby 是不同的。由于其超群的设计,Async Ruby 不受任何这些 *-地狱的困扰。它允许编写出令人惊喜的干净、简单且有序的代码。它是一个像 Ruby 一样优雅的 async 实现。
注意:Async 不能绕过 Ruby 的全局解释器锁( GIL )。
译者注:
让我们从一个简单的例子开始:
require "open-uri" start = Time.now URI.open("https://httpbin.org/delay/1.6") URI.open("https://httpbin.org/delay/1.6") puts "Duration: #{Time.now - start}"
上述代码正在发起两个 HTTP 请求。单个 HTTP 请求的总持续时间为 2 秒,包括:
让我们运行这个示例:
持续时间:4.010390391
如预期,程序需要 2 x 2 秒 = 4 秒才能完成。
这段代码还不错,但它运行速度慢。对于这两个请求,执行过程大概像这样:
问题在于程序在大部分时间里都处于等待状态; 2 秒钟(对于计算机)就像永恒。
提高多个网络请求速度的常用方法是使用线程。以下是一个示例:
require "open-uri" @counter = 0 start = Time.now 1.upto(2).map { Thread.new do URI.open("https://httpbin.org/delay/1.6") @counter += 1 end }.each(&:join) puts "Duration: #{Time.now - start}"
代码的输出是:
持续时间: 2.055751087
我们将执行时间缩短到 2 秒钟,这表明请求在同时运行。那么,问题解决了吗?
好吧,别过于着急:如果你做过任何真实世界的线程编程,你会知道线程很难。真的,非常难。
如果你打算做任何严肃的线程工作,你最好习惯使用互斥( mutexes ),条件变量( condition variables ),处理语言级的竞态条件( race conditions )...甚至我们的简单示例在 @counter += 1 这一行就有一个竞态条件错误!
线程是困难的,并且毫无疑问下面的声明在 Ruby 社区一直被不断提及:
I regret adding threads. — Matz
鉴于所有的线程复杂性,Ruby 社区早就应该有一个更好的并发模式。有了 Async Ruby ,我们终于有了一种。
让我们看看使用 Async Ruby 来进行两次 HTTP 请求的同样的例子:
require "async" require "async/http/internet" start = Time.now Async do |task| http_client = Async::HTTP::Internet.new task.async do http_client.get("https://httpbin.org/delay/1.6") end task.async do http_client.get("https://httpbin.org/delay/1.6") end end puts "Duration: #{Time.now - start}"
示例的输出是:
持续时间:1.996420725
看看总运行时间,我们可以看出请求是同时运行的。
这个例子显示了 Async Ruby 程序的一般结构:
Async
块开始。task.async
生成更多的 Async
任务。一旦你习惯了,你会发现这个结构实际上非常整洁。
前一个例子中可以被认为是一个缺点的事情是,它使用了 async-http
,一个具有异步特性的 HTTP 客户端。我们大多数人有自己喜欢的 Ruby HTTP 客户端,我们不想再花时间去学习另一个 HTTP 库的详细情况。 让我们看收同样的例子,只是这次使用 URI.open:
require "async" require "open-uri" start = Time.now Async do |task| task.async do URI.open("https://httpbin.org/delay/1.6") end task.async do URI.open("https://httpbin.org/delay/1.6") end end puts "Duration: #{Time.now - start}"
与前一个例子的唯一区别是,我们用 Ruby 的标准库中的方法 URI.open
替换了 async-http
。
示例的输出是:
持续时间:2.030451785
这个持续时间显示了两个请求是并行运行的,所以我们认为 URI.open
是异步运行的!
这一切真的很好。我们不仅不需要忍受线程及其复杂性,而且我们可以使用 Ruby 的标准 URI.open
来运行请求,
无论是在 Async
块的外部还是内部。这无疑可以为我们提供一些方便的代码重用。
虽然 URI.open
是普通的 Ruby ,但可能并不是你喜欢的进行 HTTP 请求的方式。而且,你也不经常看到它被用于"serious work(正式的工作)"。
你可能有你自己喜欢的 HTTP gem ,你可能会问 "它能在 Async 中工作吗"?为了找出答案,这里有一个使用 HTTParty
(一种知名的 HTTP 客户端)的例子。
require "async" require "open-uri" require "httparty" start = Time.now Async do |task| task.async do URI.open("https://httpbin.org/delay/1.6") end task.async do HTTParty.get("https://httpbin.org/delay/1.6") end end puts "Duration: #{Time.now - start}"
在这个例子中,我们在一起运行了 URI.open
和 HTTParty
,这完全没问题。
输出是:
持续时间:2.010069566
它运行的时间稍微超过了 2 秒,这表明两个请求是并发运行的(同时进行)。 这里的要点是:你可以在一个 Async 上下文中运行任何 HTTP 客户端,它将会异步运行。Async Ruby 完全支持任何现有的 HTTP gem !
到目前为止,我们只看到 Async Ruby 用各种 HTTP 客户端进行请求。让我们揭示 Async 在 Ruby 3 中的全部能力。
require "async" require "open-uri" require "httparty" require "redis" require "net/ssh" require "sequel" DB = Sequel.postgres Sequel.extension(:fiber_concurrency) start = Time.now Async do |task| task.async do URI.open("https://httpbin.org/delay/1.6") end task.async do HTTParty.get("https://httpbin.org/delay/1.6") end task.async do Redis.new.blpop("abc123", 2) end task.async do Net::SSH.start("164.90.237.21").exec!("sleep 1") end task.async do DB.run("SELECT pg_sleep(2)") end task.async do sleep 2 end task.async do `sleep 2` end end puts "Duration: #{Time.now - start}"
我们扩展了包含 URI.open 和 HTTParty 的前一个例子,增加了五个附加操作:
这个例子中的所有操作也需要恰好 2 秒才能运行。
以下是示例输出:
持续时间:2.083171146
我们得到的输出结果和之前一样,这表明所有的操作都是并发运行的。哇,这有很多不同的 gem 可以异步运行!
重点:任何阻塞操作( Ruby 解释器等待的方法)都与 Async 兼容,并将在 Ruby 3.0 和更高版本的 Async 代码块中异步工作。
性能看起来很好:7 x 2 = 14 秒,但示例在 2 秒内完成 – 很容易得到 7 倍的提升。
让我们花一点时间来反思一些重要的事情。这个例子中的所有操作(例如,URI.open ,Redis ,sleep )都会根据上下文的不同而表现不同:
但是,例如,HTTParty
或 sleep
方法如何能同步和异步同时存在呢? Async 库是否对所有这些 gems 和内部 Ruby 方法进行了猴子补丁? 这种魔术是由于 Fiber Scheduler
。这是 Ruby 3.0 的一个特性,使得 async
能够很好地与现有的 Ruby gems 和方法集成 - 不需要任何 hack 或 猴子补丁(Monkey patch) !
Fiber Scheduler 也可以单独使用 (链接译文)!用这种方式,只需要几个内置的 Ruby 方法就能启用异步编程。
如你所想,Fiber Scheduler 触及的代码范围非常广:它是 Ruby 当前所有的阻塞 API !这绝不仅仅是一个小功能。
让我们提高效率,并展示一个 Async Ruby 擅长的另一方面:扩展(scaling)。
require "async" require "async/http/internet" require "redis" require "sequel" DB = Sequel.postgres(max_connections: 1000) Sequel.extension(:fiber_concurrency) # Warming up redis clients redis_clients = 1.upto(1000).map { Redis.new.tap(&:ping) } start = Time.now Async do |task| http_client = Async::HTTP::Internet.new 1000.times do |i| task.async do http_client.get("https://httpbin.org/delay/1.6") end task.async do redis_clients[i].blpop("abc123", 2) end task.async do DB.run("SELECT pg_sleep(2)") end task.async do sleep 2 end task.async do `sleep 2` end end end puts "Duration: #{Time.now - start}s"
此例子基于之前的那个例子,只是做了一些改动:
Async
区块中的所有内容都会被重复 1000.times
(运行 1000 次),这将并发操作的数量增加到了 5000 。URI.open
和 HTTParty
替换为了 async-http
HTTP 客户端。async-http
可以与 HTTP2 一起工作,当进行大量请求时,它的速度要快得多。就像之前一样,每个独立操作都需要 2 秒才能执行。其输出为:
持续时间: 13.672289712
这表明累积运行时间为 10,000 秒的 5,000 个操作仅在 13.6 秒内就完成了!
这个持续时间比前面的例子( 2 秒)要长,这是因为创建这么多网络连接的开销。
我们几乎没有进行性能调优(例如,调整垃圾收集,内存分配等),但我们仍然实现了 730 倍的“加速”,在我看来,这是一个相当令人印象深刻的结果!
最好的部分是:我们只是初步探索了使用 Async Ruby 所能做到的事情。
虽然线程( Threads )的最大数量是 2048 (至少在我的机器上是这样),但是 Async tasks 的上限数量是百万级别的!
你真的可以同时运行百万个异步操作吗?是的,你可以 - 已经有些用户做到了。
Async 真的为 Ruby 打开了新局面:想象一下一个 HTTP 服务器处理成千上万的客户,或者同一时间处理成百上千的 websocket 连接 ... 这都是可能的!
Async Ruby 经过了漫长而神秘的开发期,但现在它稳定且已经准备好投入生产。已经有一些公司在生产环境下运行它并从中受益。要开始使用它,你可以去 Async 的仓库看看。
唯一的注意点是,它不能和 Ruby on Rails 一起工作,因为 ActiveRecord
不支持 Async
gem 。但如果不涉及到 ActiveRecord
,你仍然可以在 Rails 中使用它。
Async 的最大优势在于扩展网络 I/O 操作,比如进行或接收 HTTP 请求。对于 CPU 密集型的工作负载,线程是更好的选择,但至少我们不再需要把他们用于所有事情。
Async Ruby 非常强大,可扩展性极高。它是一个游戏规则改变者,我希望这篇文章能证明这一点。Async 改变了 Ruby 的可能性,并且当我们所有人开始更多地“异步”思考时,它将对 Ruby 社区产生重大影响。
最好的一点是,它不会使任何现有的代码变得过时。就像 Ruby 本身一样,Async 设计得很美,使用起来也很愉快。
希望你在使用 Async Ruby 时编程愉快!
Happy hacking with Async Ruby!
]]>Fiber Scheduler(纤程调度器)在 Ruby 中实现异步编程。该功能是 Ruby 3.0 的一大增强功能,并且也是优秀的 async gem 的核心组件之一。 最棒的一点是,你并不需要一个完整的框架就能开始!只需使用一对内置的 Ruby 方法,就能独立地实现纤程调度器并享受到异步编程的好处。
纤程调度器主要包括两部分:
Fiber Scheduler interface (纤程调度器接口) 这是一套内置于编程语言中的阻塞操作钩子。钩子实现被委托给 Fiber.scheduler
对象。
Fiber Scheduler implementation (纤程调度器的实现) 实现了异步行为。这是一个需要程序员显式设置的对象,因为 Ruby 不提供默认的 Fiber Scheduler (纤程调度器)实现。
非常感谢 Samuel Williams !他是 Ruby 的核心开发者,设计并实现了纤程调度器这一功能并整合到了语言中。
Fiber Scheduler (纤程调度器)接口是一套阻塞操作的钩子,它允许在阻塞操作发生时插入异步行为。它像是带有反转的回调:当异步回调被执行时,主阻塞方法不会运行。 这些钩子在 Fiber::SchedulerInterface 类中有文档记录。这个 Ruby 功能背后的一些主要思想包括:
#address_resolve
钩子负责处理大约 20 个方法。Fiber.scheduler
对象设置后才会工作,钩子的实现被委托给该对象。让我们看一个示例,显示如何实现 Kernel#sleep
钩子。在实践中,所有的钩子都是用 C 语言编写的,但为了清晰起见,这里使用了 Ruby 伪代码。
module Kernel def sleep(duration = nil) if Fiber.scheduler Fiber.scheduler.kernel_sleep(duration) else synchronous_sleep(duration) end end end
以上代码的阅读方式如下:
Fiber.scheduler
对象 - 运行其 #kernel_sleep
方法。#kernel_sleep
应该异步运行 sleep
。synchronous_sleep
,它会阻塞当前线程直到 sleep
完成。其他的钩子的工作方式也类似。
已经多次提到了"Blocking operations (阻塞操作)"这个概念,但它到底是什么意思呢?阻塞操作是指任何 Ruby 进程(更具体地说:当前线程)最终会等待的操作。一个更具描述性的名称是“waiting operations (等待操作)”。 一些例子如下:
sleep
方法。URI.open("https://brunosutic.com")
。curl https://www.ruby-lang.org
。Thread#join
等待线程结束。作为一个反例,以下代码片段需要一段时间才能完成,但不包含阻塞操作:
def fibonacci(n) return n if [0, 1].include? n fibonacci(n - 1) + fibonacci(n - 2) end fibonacci(100)
获取 fibonacci(100)
的结果需要等待很长时间,但只有程序员在等待!整个时间 Ruby 解释器都在工作,后台进行计算。一个简单的斐波那契实现并不包含阻塞操作。
发展对阻塞操作是什么(和不是什么)的直觉是值得的,因为异步编程的整个目标就是同时等待多个阻塞操作。
纤程调度器实现是 Fiber Scheduler 功能的第二大部分。
如果你想在 Ruby 中启用异步行为,你需要为当前线程设置一个 Fiber Scheduler
对象。这是通过 Fiber.set_scheduler(scheduler)
方法完成的。实现通常是一个定义了所有 Fiber::SchedulerInterface
方法的类。
Ruby 不提供默认的 Fiber Scheduler
类,也没有可以用于此目的的对象。这看起来不寻常,但实际上不将 Fiber Scheduler
实现包含在语言中是一个好的长期决定。最好将这种相对快速演变的关注点留在 Ruby 核心之外。 从头开始编写 Fiber Scheduler
类是一项复杂的任务,所以最好使用现有的解决方案。实现的列表,它们的主要区别和推荐可以在 Fiber Scheduler List 项目中找到。
让我们来看看仅使用 Fiber Scheduler
可以做什么。 所有示例都使用 Ruby 3.1 和来自 fiber_scheduler gem 的 FiberScheduler
类,这个 gem 由我维护。这个 gem 对于示例来说不是一个硬性依赖项,因为如果将以下代码片段中的 FiberScheduler
替换为另一个 Fiber Scheduler
类,每个代码片段仍然应该可以工作。
这里有一个简单的示例:
require "fiber_scheduler" require "open-uri" Fiber.set_scheduler(FiberScheduler.new) Fiber.schedule do URI.open("https://httpbin.org/delay/2") end Fiber.schedule do URI.open("https://httpbin.org/delay/2") end
上面的代码创建了两个纤程,每个纤程都进行一次 HTTP 请求。这些请求并行运行,整个程序在 2 秒内完成。
Fiber.set_scheduler(FiberScheduler.new)
在当前线程中设置一个 Fiber Scheduler
,这使得 Fiber.schedule
方法可以工作,且 fiber 可以异步行为。
Fiber.schedule { ... }
这是一个内置的 Ruby 方法,用于启动新的异步 fiber 。
这个示例仅使用了标准的 Ruby 方法 - Fiber.set_scheduler
和 Fiber.schedule
自 Ruby 3.0 版本以来就一直可用。
我们来看看运行多种不同操作是什么样子的:
require "fiber_scheduler" require "httparty" require "open-uri" require "redis" require "sequel" DB = Sequel.postgres Sequel.extension(:fiber_concurrency) Fiber.set_scheduler(FiberScheduler.new) Fiber.schedule do URI.open("https://httpbin.org/delay/2") end Fiber.schedule do # Use any HTTP library HTTParty.get("https://httpbin.org/delay/2") end Fiber.schedule do # Works with any TCP protocol library Redis.new.blpop("abc123", 2) end Fiber.schedule do # Make database queries DB.run("SELECT pg_sleep(2)") end Fiber.schedule do sleep 2 end Fiber.schedule do # Run system commands `sleep 2` end
如果我们顺序运行这个程序,它大约需要 12 秒才能完成。但是由于这些操作是并行运行的,所以总的运行时间仅仅超过 2 秒。 你并不仅限于发起 HTTP 请求。任何内置在 Ruby 中或由外部 gem 实现的阻塞操作都可以工作!
这是一个简单的,显然是人为刻意的示例,同时运行一万个操作。
require "fiber_scheduler" Fiber.set_scheduler(FiberScheduler.new) 10_000.times do Fiber.schedule do sleep 2 end end
上述代码的完成时间略超过 2 秒。
由于其低开销,sleep
方法被选择用于扩展示例。如果我们使用网络请求,由于需要建立数千个连接并进行 SSL 握手等,执行时间将会更长。
异步编程的主要优势之一是能够同时等待许多阻塞操作。阻塞操作数量的增加将增加这种优势。幸运的是,运行大量协程(fibers)非常简单。
Ruby 只需要一个纤程调度器( Fiber Scheduler )和一些内置方法就可以异步工作 - 不需要任何框架!
使其工作很容易。选择一个纤程调度器( Fiber Scheduler )实现,然后使用以下这些方法:
Fiber.set_scheduler(scheduler)
为当前线程设置一个纤程调度器( Fiber Scheduler ),使阻塞操作能够异步执行。Fiber.schedule { ... }
启动一个新的纤程,该纤程与其他纤程并发运行。一旦你开始运行,你可以通过将它包装在一个 Fiber.schedule
块中来使任何代码异步化。
Fiber.schedule do SynchronousCode.run end
整个库可以轻松地使用这种方法转换为异步,而且往往不需要比这里展示的更多努力。
异步编程的重大好处是并行化阻塞/等待操作以减少程序运行时间。这通常意味着在单个 CPU 上运行更多的操作,或者更好地,在你的 Web 服务器上处理更多的请求。
祝你使用纤程调度器( Fiber Scheduler )愉快!
Happy hacking with Fiber Scheduler!
]]>大家好,我是 Mark24
我们都知道 Javascript 是单线程的。
今天看到一个有趣的帖子 www.v2ex.com/t/871848,主要是争论 Javascript 的优缺点。我看到这个评论觉得很有意思:
@qrobot: ....省略.... 多线程下会消耗以下资源 1. 切换页表全局目录 2. 切换内核态堆栈 3. 切换硬件上下文(进程恢复前,必须装入寄存器的数据统称为硬件上下文) ip(instruction pointer):指向当前执行指令的下一条指令 bp(base pointer): 用于存放执行中的函数对应的栈帧的栈底地址 sp(stack poinger): 用于存放执行中的函数对应的栈帧的栈顶地址 cr3:页目录基址寄存器,保存页目录表的物理地址 ...... 4. 刷新 TLB 5. 系统调度器的代码执行 ....省略.....
这位同学列举了多线程切换的时候发生了什么。 这样给了一种很直观的感受,就是多线程切换的时候发生了很多事情,实际上会比单线程(只需要切换函数上下文)要消耗点更多的资源。
实际上凡是交互的软件,最终都是 单线程模型 + 事件驱动辅助。
从熟悉的浏览器、游戏、应用程序……都是如此。
也有多线程实现的。这里Multithreaded toolkits: A failed dream? (2004) 有很多讨论。
实际上单线程模型是最后的胜出者。
Javascript 内部单线程处理任务,主要是有一个 EventLoop 单线程的循环实现。
我们可以通过 Javascript 的表现,反推实现一下 EventLoop 。
我们知道 setTimeout
在 Javascript 中用来推迟任务。实际上自从 Promise 出现之后,渐渐有两个概念出现在大家的视野里。
setTimeout 属于宏任务,而 promise 的 then 回调属于微任务。
还有一个就是 Javascript 在第一次同步执行代码的时候,是宏任务。
EventLoop 的表现是,除了第一次执行结束之后,如果有更高优先级的 微任务总是先执行微任务,然后再执行宏任务。
setTimeout 是一个定时器,很特别的是他在会在计时器线程工作,运行时间之后,回调函数会被插入到 宏任务中执行。计时器线程其实不是 Javascript 虚拟的一部分,他是浏览器的部分。
Javascript 是单线程的。Ruby 是支持多线程的。我们可以用 Ruby 模拟一个 单线程的核心,和单独的计时器线程,这都是很轻松的事情。
其实我们听到了这个行为 —— 花 1 分钟大概能想到,EventLoop 的工作模型
我们可以用数组当做 队列。但是 由于存在时间线程,还得用 Thread#Queue 有保障一点。
大概的模型可以画出来,想这个样:
( start) | init (e.g create TimerThread ) | sync task (e.g read & run code) | | ------------------> | | ------------- | macro_task --- add timer task --> | TimerThread | | (Eventloop) | <-- insertjob result --- ------------- | | | micro_task | | | | <----------------- | | (end)
然后我们大概用 100 行不到就可以实现如下:
settimeout 不要用每一个新的线程来模拟,因为一旦多线程,涉及到抢占式回调,其实返回的时间不确定。你的结果是不稳定的。 我们需要单独实现一个计时器线程。
我们通过行为封装,把两边函数写法对照,这样可以复制
运行看结果
# https://github.com/Mark24Code/rb_simulate_eventloop require 'thread' class EventLoop attr_accessor :macro_queue, :micro_queue def initialize @running = true @macro_queue = Queue.new @micro_queue = Queue.new @time_thr_task_queue = Queue.new @timer = Timer.new(@time_thr_task_queue, @macro_queue) # 计时线程,是一个同步队列 # 会把定时任务结果塞回宏队列 @timer_thx = Thread.new do @timer.run end end def before_loop_sync_tasks # do sth setting @first_task.call end def task(&block) # 这里放置第一次同步任务 # # 外部书写的代码,模拟读取 js # 提供内部的 api @first_task = -> () { instance_eval(&block) } end def after_loop puts "[after_loop] eventloop is quit :D" end def macro_queue_works while !@macro_queue.empty? job = @macro_queue.shift job.call end end def micro_queue_works while !@micro_queue.empty? job = @micro_queue.shift job.call end end def start begin before_loop_sync_tasks while @running macro_queue_works micro_queue_works # avoid CPU 100% sleep 0.1 end ensure after_loop end end # dsl public api # inner api def macro_task(&block) @macro_queue.push(block) end def micro_task(&block) @micro_queue.push(block) end def settimeout(time, &block) # 模拟定时器线程 if time == 0 time = 0.1 end # 方案 1: 用独立分散的线程模拟存在问题 # 抢占的返回顺序不是固定的 # t = Thread.new do # sleep time # @micro_queue.push(block) # end ## !!! 这里一定不能阻塞,一旦阻塞就不是单线程模型 ## 有外循环控制不会结束 # t.join # 方案 2: 时间线程也需要单独模拟 # 建立一个时间任务 @time_thr_task_queue.push({ sleep_time: Time.now.to_i + time, job: -> () { @micro_queue.push(block) } }) end end class Timer def initialize(task_queue, macro_queue) @task_queue = task_queue @macro_queue = macro_queue end def run while (task = @task_queue.shift) sleep_time = task[:sleep_time] if sleep_time >= Time.now.to_i @macro_queue.push(task[:job]) else @task_queue.push(task) end end end end
选择单线程的原因是因为
Nginx 、Redis 内部也实现了单线程模型,来应对大量的请求,提高并发。
现在我们大概知道了,浏览器、应用、app 、图形界面、游戏……
他们的背后大概是什么样子。 破除神秘感 +1 :D
]]>本文比较啰嗦,更倾向于是自言自语。不过我写完回顾,这更像是这段时间,自由思考的总结 :P
不过我不是游戏领域的人,这部分都是业余摸鱼思考的记录,如果有勘误,请与我联系,非常乐意交流。
文章可能需要 30 分钟。
主要涉及的主题:
使用 Ruby 实现 demo 。
项目安装: gem install rb2048
帮助信息: rb2048 --help
Usage: rb2048 [options] --version verison --size SIZE Size of board: 4-10 --level LEVEL Hard Level 2-5
开始游戏 rb2048
-- Ruby 2048 -- ------------------------------------- | 16 | 16 | 2 | 16 | ------------------------------------- | 0 | 0 | 0 | 0 | ------------------------------------- | 0 | 0 | 0 | 2 | ------------------------------------- | 0 | 0 | 0 | 0 | ------------------------------------- Score: 16 You:UP Control: W(↑) A(←) S(↓) D(→) Q(quit) R(Restart)
升级难度 rb2048 --size=10 --level=5
-- Ruby 2048 -- ----------------------------------------------------------------------- | 8 | 16 | 0 | 0 | 0 | 0 | 0 | 2 | 0 | 0 | ----------------------------------------------------------------------- | 0 | 16 | 0 | 16 | 0 | 8 | 0 | 0 | 0 | 0 | ----------------------------------------------------------------------- | 0 | 0 | 0 | 2 | 0 | 0 | 0 | 0 | 16 | 8 | ----------------------------------------------------------------------- | 0 | 16 | 0 | 8 | 0 | 0 | 0 | 0 | 0 | 2 | ----------------------------------------------------------------------- | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ----------------------------------------------------------------------- | 0 | 8 | 8 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ----------------------------------------------------------------------- | 8 | 0 | 0 | 0 | 0 | 4 | 0 | 0 | 0 | 0 | ----------------------------------------------------------------------- | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | ----------------------------------------------------------------------- | 0 | 0 | 0 | 4 | 0 | 0 | 0 | 0 | 0 | 0 | ----------------------------------------------------------------------- | 0 | 4 | 0 | 0 | 4 | 8 | 0 | 0 | 0 | 16 | ----------------------------------------------------------------------- Score: 0 Control: W(↑) A(←) S(↓) D(→) Q(quit) R(Restart)
我觉得命令行的程序比较赛博朋克,一直想做个命令行的交互程序。 目前在游戏公司,虽然我不是游戏工程师,但是接触了一些游戏行业的优秀小伙伴,我也忍不住思考关于游戏的主题。
我想做的命令行交互式程序,其实和游戏的思想内核是一致的。一拍即合。
我以前做过一点点研究。记录了一些笔记。关于 Ruby 中如何实现交互式命令行程序。 本文也是建立在这个基础之上。
用最简单的方式实现了一个 [贪吃蛇]
rb2048 有趣的地方在于,在设计的时候,没有简单实现了之。毕竟有太多 2048 了,不差这一个。
对于我不是完成一个任务。由于最近两天关注于线程的使用,于是我把线程方面的使用加入到 rb2048 。这算是一个实验性的例子。验证我的想法:
rb2048 将:
这三部分分别用单独的线程实现,用队列通信。麻雀虽小,五脏俱全。虽然粗糙,但是代表了游戏引擎典型的设计思路。 (虽然我了解的不多)
简单说说我最近的思考吧:
1 )对于计算机不同领域认识发生了变化
以前会觉得:游戏是游戏,web 是 web ,语言是语言,元编程就是元编程……也许还有很多概念,但是渐渐现在觉得无非是一件事 —— 编程罢了。
随着看到思考的东西逐渐变多,很多计算机领域的问题,在我的角度觉得都一样。
2 )第一性原理 + 交流,向内习得
这次摸着石头过河,比较新奇的体验就是,从当初一个想法到原理的讨论到最后实现。主要是思考推理,还有和优秀的同事的聊天中习得 (这里感谢 @谷神)。
现实中有很多游戏引擎。他们也许内有乾坤,不过其实是否研究他们也不重要。
我也不在乎别人的实现,或者更好地实现,是否有实现过了可以参考。其实没什么可参考的。只要我们自己想明白了,别忘了我们上面说的,他们都是一件事 —— 编程罢了。 当我们面临新问题,我们也会加强我们的 “引擎”。从思想上,他们是平等的。:P
可能与以前向外求知,现在会额外的向内思考。比较神奇的体验是,一些东西听个大概,也能盲猜个七八分。
其实 2048 没啥好聊,写 2048 的背后是对游戏的一些思考。
其实游戏是一个比较特别的存在。他是一种比较特殊的程序,特殊在哪儿呢?
1 )他是持续交互程序
不同于简单的脚本,跑完结束。或者传递一个初始参数,就像函数一样运行完结束。
他是一个持续交互的过程,随着时间累计游戏的方方面面都在变化。
2 )多面平衡
不同于你写一段 function 就结束了。游戏要在运行的生命周期里:
在至少这三个方面互相作用。
还可能有:
其他周边并不展开
3 )稳定的帧率
如果是 60HZ 的游戏,必须在 16.6ms 内完成动作进行刷新。
这也不是普通业务脚本、程序一直跑自己的线性逻辑就算了,根本不关心时间。
4 )密集对象计算
简单的游戏还好,传统的模式是面向对象建模,一切看起来还算自然。
但是也出现了万人同台的游戏,这里传统的编程模式已经满足不了游戏对象的遍历了,很快会达到性能瓶颈。
这几年,出现了 ECS 架构( Entity-Component-System )。
小结:
其实还有各种发散。如何使用 CPU 、GPU 加速渲染,这就不再提了。
游戏是一个非常特殊的存在,它意味着密集型计算、密集型 IO 混合出现的场景。我理解是比 Web 复杂在另一个维度上。
游戏涉及到 编程架构、网络、图形学、美术设计、资源加载…… 诸多丰富的话题。
这些就不是我这个门外汉靠管窥蠡测能够说得清的。我今天可以只谈谈我对游戏的理解和认识,以及构建 2048 的思考。
其实一个基本游戏可以用如下代码描述:
loop do IOEvent UpdateGameData Render end
游戏处在一个主循环中,我们依次要处理用户输入事件,根据用户输入事件进行游戏模型的变化,最后再把数据渲染在屏幕上。
这是一个单线程,主循环的例子。
现实中每个部分都可以额外变得复杂。也可以用线程单独实现。一切看需求。
你会发现游戏就是交互程序。
上面的三部分,你也可以和 MVC 强行扯在一起。
MVC 的典型程序,除了桌面软件,Web 也算是,App 也算。
看似是在说游戏,实际上他们是一回事。
游戏引擎其实就是框架,很佩服他们会起名字。
框架、引擎其实是一个东西,他们的特征就是一个半成品的软件。
loop do IOEvent UpdateGameData Render end
比如这个游戏循环,如果我们封装了主循环,封装了事件对象。对外暴露了一些生命周期。 这种半成品软件就是 所谓的框架,在游戏领域就是引擎。
作为下游,游戏引擎 /框架的使用者来说,我们写的程序就像填空一样和主循环工作在一起。
所以我个人觉得,决定了什么是 框架 Framework 和 库 Library 的本质区别是 —— 主循环。
当你的程序是一种可被调用的状态,那么基本上你的程序可以看成一个 lib 当你的程序如果拥有了主循环的状态,基本宣告了不可被直接调用。那么它其实是一个 Framework 了。除了各种 Pattern 很少见到主循环的 lib 展示,不存在的原因是因为拥有主循环的程序,一般以具体的软件形态出来:
Framework 式的程序,你的工作任务就会转向熟悉这个程序暴露的对象,期待你的程序和主循环能一起工作。
我们再来聊聊游戏引擎和编程语言。
Unity 的背后是 C# 支撑;虚幻引擎的背后是 C++。他们采用了更底层的语言。那么问题来了,编程语言会成为制约游戏的瓶颈么?
这也是我自己思考的一个问题。
我们可能会很粗暴地觉得 动态语言普遍慢,当然是越接近底层越好。其实我更想知道,如此这样选择的标准在哪儿?
其实我们可以思考下,这个结论不难获得。
其实动态语言在执行一个命令的时候,Ruby 这种最后 C 实现; Golang 最后也落在 C ( Golang 实现自举之后,那就用汇编思考吧)。其实他们在执行一个具体操作的时候,数量级一致的。
他们其实差不多。
速度差距在哪儿呢?
1 )载入环境
C 、Golang 这种可以打包成二进制的语言。他编译阶段会把需要执行的代码编译成二进制。
所以执行的时候载入的是所需要用到的部分功能。
Python 、Ruby 这种其实 二进制是语言的解释器。运行的时候更多的时间花费在加载解释器。
不过,当你的程序复杂到涉及大量 IO 、基础库的时候,Golang 的打包结果会趋向于接近一个解释器的大小,比如 Ruby 差不多在 30M 左右。
我曾经比较过:
Golang 的一个项目命令行编辑器 micro 、Ruby 的一个项目命令行编辑器 diakonos
micro 运行内存 16M ,也就是他本地大小; diakonos 运行内存 30M ,也就是 Ruby 解释器差不多的大小。ruby 代码会执行才加载,所以可以忽略不计。
最大的差距,在于 30-16 的载入速度差,这个量级是不同的。
2 )语言构件
C 语言就像是一个高级一点的汇编。C 的角度一切都需要手动管理。那么其实对于底层语言,更现实一点的是会自己手动实现数据结构。
Ruby 这种动态语言,内部默认会有一个数据结构。
举个例子:
比如 a = "GAME"
C 语言实际上只会手动创建 "GAME" 四个字符
Python 底层可能创建一个 20 字符长度的数组。存 GAME 。也有好处,可以不定长支持动态扩容。
在生成语言构建的时候存在速度差。 动态语言等于多创建了很多语言在内存里的解构。
3 )解析时间
二进制的文件,直接载入内存执行。
动态语言有一个解析的过程。当然,也有优化空间,我们可以提前编译动态语言为虚拟机字节码。这样就获得了 对于解释器是二进制类似的东西。
4 ) GC 时间
和 C 语言相比,Python 、Ruby 自带 GC 。
他们存在一个 必须 GC 暂停的那么一个问题。C 语言的策略是手动回收。
我们好像列举了一大堆 动态语言的缺点似的。实际上自动管理的数据结构、自带 GC 、可以动态的编译执行…… 这些都是动态语言的缺点。
虽然付出了些许时间的代价。只要我们不滥用语言构件 和 特别烂的算法,真是巧妙的接近底层高效的实现。
其实我想说,动态语言至少在目标上不是特别大的瓶颈。
Java 也有游戏的例子; C# 也是自带 GC 。GC 不会是瓶颈。
语言的速度不会绝对意义上成为一个游戏组成的阻碍。
EVE 这样的大型游戏,内部使用了 巨慢的 Python 就可以说明问题。
之所以语言不一定构成拖慢游戏的原因,还有一个就是游戏和屏幕的刷新机制 —— 双缓冲模式。
其实可以理解为一个 内存空间,我们称之为 Buffer 。我们有两个 Buffer ,分别叫 A Buffer 、B Buffer 。
显示器先从 A Buffer 中读取数据渲染屏幕。我们程序写入 B Buffer ,等我们真的写完了,可慢或者快,但是无所谓,反正屏幕这时候在稳定的读取 A Buffer 内容。我们计算完毕,B Buffer 中写入了我们想要的东西,这时候只要把显示器读取的指针指向 B Buffer ,下次屏幕就会获得我们想要的画面。这就是双缓冲模式。由于存在双缓冲解构,算快和快慢,至少不会成为画面撕裂的原因。
rb2048 使用了 Curses 库来绘制界面,而 Curses 内部使用了双缓冲模式。
我们自己研究了两天线程和队列。主要是 Ruby 的实现。
这里不教线程和协程,只记录我觉得好玩的交流结果。
缺点:
Ruby 存在线程锁,这导致每一时刻只能运行一个线程。线程就像背后虽然有很多工人,但是只能交替的一人一锤子。
这背后的原因在于 Ruby 考虑安全更多一点 —— 线程安全。
这样的多线程无法利用 CPU 多核心并行的特点。希望利用多核的,可以去用 JRuby ,因为 Java 底层没有加锁。
Ruby3 中也有了无锁线程的替代品 Ractor 也可以了解下。
CRuby 如果想利用多核心可以使用进程替代线程。如果设计得当,其实差不多。Ruby 里面 Webserver 有名气的 Puma 采用的就是多进程实现。
优点:
加上锁最大好处是线程安全,你可以自由的编码,Ruby 帮你加锁。这样多线程访问变量的时候,不会出错。
但是你退出来想,反正你自己也要加锁啊,谁加不是加。Ruby 默认的线程其实书写起来非常友好。
我觉得再这样介绍这三个概念,这文章太冗长了。
直接说结论吧,直观上,这三者存在量级差,不仅体现在空间资源,时间资源都差不多。
进程 >> 线程 >> 协程
比如一台机器 4G 内存:
可能只能实际生成几百个进程就不太行了。 同样,可以生成几千个线程,就动不了了。 协程可以生成几十万个。
他们大概就是这个差距(有更好数据支持的,请联系我)。
他们切换上下文的时间也遵循这个比较关系。
所以我们一般的策略,尽量多用协程&线程,少用进程。
如果任务独立运行还好,就怕彼此还要通信,出现互相等待的局面。
线程具有 CPU 亲和性(一般语言来讲)。
比如 Golang 的 M:N 模型,主张 先生成 M 个线程,M 是机器 CPU 核心数,然后再在 M 个线程之间调度实际产生的 N 个任务。
比如 Nginx 的配置也主张 配置线程核心数和 CPU 核心数一致。
线程、协程产生的原因是什么?
其实还是为了调度。
线程是细分进程下共享内存的场景;协程是为了细化调度。
因为进程、线程本质上是操作系统在调度。操作系统并不清楚什么时候应该调度。只能采用各种优先计算法、平均算法。再怎么算,也是盲人摸象罢了。
协程给了程序员一个口子,你可以用 协程在 涉及阻塞部分进行让出控制权。
简而言之,经验之谈:
涉及到 计算密集型 请用线程。
如果涉及到 IO 阻塞密集,请用协程。
我们的目的不是为了用而用,而是使用调度,提高我们代码执行的效率,减少等待。
如果说其实没有 if-else\switch\while ,计算机器其实只有 goto 。
如果你看过汇编,大概理解我是什么意思。
同样,计算机里进程、线程、协程背后调度的秘密,都来自于 CPU 的硬件中断功能。
只不过是上下文快速切换,切换上下文多和少罢了。
其实 2048 的关键就是相邻元素合并,实现这么一个算法,反复执行到无元素可以继续合并。再把这个应用到 x\y 方向所有行列就好了。
目前实现成通过队列来实现通信:
IO 线程,用户产生一个输入,进入事件队列。 游戏读取事件队列,开始计算游戏数据,把结果塞入渲染队列。 渲染线程,读取渲染队列数据进行渲染。
我和同事交流了一下,就 2048 而言其实可以很多方式做:
我们等于做出一个 pipline 的方式了
真正的自由渲染。虽然 2048 看不出效果
用户不断地敲击,产生时间,如果队列里一致产生数据,那不是渲染永远追不上?
多线程队列需要思考 生产者、消费者模型,需要设计匹配的方式。
解决方法
1 )控制生产频率,生产和消耗相抵消
事件采样、渲染 可以保持一个频率
2 )不控制生产,但是跳过生产
事件采样,可以携带时间戳。
如果渲染的时候,每次时间超时,跳过关键帧。
当然这些都是很细化的问题了。
我倾向于研究一个东西,思考他的全部,寻找最佳的路径。 这些都是摸鱼结果,简单分享下。更深的感受还需要实践和交流。
上文提到游戏里面最新流行 ECS 架构。ECS 抛弃了面向对象的思想,把同类数据摆放在一起,亲和 CPU 运行机制,方便大规模属性遍历。
ECS 应该如何用 Ruby 实现呢?
]]>阅读大概需要 20 分钟。
假设你希望了解 线程、线程池、集群模式 /Master-Worker 模式、调度器。
需要了解 Ruby 基本的用法和面向对象思想。
本文戏说,无须严肃对待。勿对号入座。个人也没有严肃观点。个人观点和所有人没有关系。
MasterWorker 模式,也有翻译成作集群模式、也叫 Master-Slave 模式。
Git 不许使用 master 了,换成了 main ,Master/Slave 具有政治不正确的歧视色彩。不过这不重要了。其实这个名字很能表达这个模式的特点。
主要思想就是由一个 Master 抽象对象来调度 Worker 对象来工作。
其实这也非常像现实中的工作模型。Ruby 天生面向对象,表达的文学性,我们可以很方便的来使用代码模拟这种现实情况。 我们来用 Ruby 模拟下现实中这种情况,顺便学下如何实现这个模式。
会出现几个类:
故事的思路:
我们自己是客户,把“任务”订单交给“公司”,这些任务会转交给“领导”手中,然后“领导”会排期,把工作布置给“打工人”。最终“打工人”乐此不疲的完成任务。
首先我们建立一个 Worker 类,我们给他一个名字属性。attr
暴露出 name
属性。
# Workshop.rb class Worker attr :name def initialize(name) @name = "worker@#{name}" end end
我们采用 TDD 方式来逐步实现我们的想法:
#Workshop_test.rb require 'minitest/autorun' require_relative '../lib/Workshop' describe Worker do it "check worker name" do w = Worker.new("ruby01") assert_equal w.name, "worker@ruby01" end end
很快,我们知道这名打工人他叫 “ruby01” 员工。
我们不希望打工人每次只能做一件事,你必须得推着他才能工作。他最好学会“成长”会自己努力的工作。 其实就是一堆任务,我们希望他们一直忙。给他 N 件事情,他一个一个自己做。 我们要给他一个目标,也就是 KPI 或者 OKR 随便吧,实际上这是一个队列对吧。我们用队列实现。
require 'thread' class Worker attr :name def initialize(name) @name = "worker@#{name}" @queue = Queue.new @thr = Thread.new { perfom } end def <<(job) @queue.push(job) end def join @thr.join end def perfom while (job = @queue.deq) break if job == :done puts "worker@#{name}: job:#{job}" job.call end end def size @queue.size end end
现在打工人变得充实了许多,他自从来了公司培训之后,就拥有了很多属性和方法。
@queue
就是他的 OKR 清单,他必须完成所有的工作任务。
@thr
意思是 thread 缩写,这里是会使用一个线程来调用 perform
我们在用线程模拟打工人干活这件事。可以理解为 @thr
就是打工人的灵魂。
<<
是一个 push 方法的语法糖,就给给自己的 OKR 里添加任务。
perform
可能要说下 perform 方法, 这里是 “运行”的意思哈,不是“表演” :P 。 打工人怎么干活呢?这得说道说道。我们得指导他如何“成长”。
我们前面说了 @queue
就是他的 OKR, 他必须从自己的 OKR 中取出任务然后执行。这里我用了 job.call
。 暗示,这必须是一个 callable 对象,在 ruby 里也就是拥有 call
方法的对象。可以是 lambda 、或者实现 call 的。 这也很合理,需求必须能做才会做。没法做的需求,做不了就是做不了。
但是如果给了一个 :done
另说。循环会结束,这个线程会消失。(裁员了 :P)
def perfom while (job = @queue.deq) break if job == :done puts "worker@#{name}: job:#{job}" job.call end end
其实 Queue 这个对象很有意思,Ruby 做了一些工作。Queue 在空的时候,虚拟机会让线程进入睡眠等待。如果队列里有任务,就会继续工作。Ruby 很贴心,果然是程序员的好朋友啊。 其实我不知道其他语言什么样,懒得查了。
join
方法是一个 Thread 的线程方法,主要的作用是告诉主线程你要等待每一个子线程(自己)的完成。如果不写这句,主线程如果比所有子线程提前结束。那么子线程会被全部关闭。简而言之 join
就是同步等待线程结果。
让我们来看看 TDD:
我们可以加一段验证工号 ruby02 的打工人是不是如期的完成了工作。
# .... it "check worekr do sth job" do w = Worker.new("ruby02") finished = [] w << lambda { puts "do job 1"; finished.push "job1"} w << lambda { puts "do job 2"; finished.push "job2"} w << :done w.join assert_equal finished, ["job1","job2"] end # ....
其实到这里,一个合格的打工人就打造完毕了。打工人很简单,只要吃苦耐劳,一切都 OK 。 下面我们要实现下 Workshop 公司类。
其实我打算过渡下,首先实现一个 “创业公司” MiniWorkshop
。 创业公司刚起步,一般是只有“打工人”,没有真正意义上的中层出现。 这一时期非常简单,伊甸园时期。有活大家一起干,大家都是兄弟。
class MiniWorkshop def initialize(count) @worker_count = count # 打工人数量 @workers = @worker_count.times.map do |i| # 根据数量生成(招聘)打工人 Worker.new(i) # 给个工号 end end # 初创公司分配任务 def <<(job) if job == :done @workers.map {|m| m << job} else # 随机选择一个打工人,接活 @workers.sample << job end end def join @workers.map {|m| m.join} end end
这里可能说下
def <<(job) if job == :done @workers.map {|m| m << job} else # 随机选择一个打工人,接活 @workers.sample << job end end
这里干活的模式可能不好,因为我们竟然 Array#sample
方式。这是一个随机方法。随机选择一个。 看似不合理,实际上也合情合理。
创业公司初期虽然是草根,可是大家哪个不是大佬。所以活来了谁都行,问题不大。
没事我们后面再改进好了。
TDD:
我们的单元测试其实描述了一个故事。一家创业公司,只有 2 个人。接到了一个订单是 4 个工作内容。
# ... it "check MiniWorkshop work" do ws = MiniWorkshop.new(2) finished = [] ws << lambda { puts "job1"; finished.push "job1"} ws << lambda { puts "job2"; finished.push "job2"} ws << lambda { puts "job3"; finished.push "job3"} ws << lambda { puts "job4"; finished.push "job4"} ws << :done ws.join assert_equal finished.size, 4 end # ...
我们回过头再看 MiniWorkshop
类,初始化的时候创建了两个员工。任务来了就随机分配给一个员工。 很符合小作坊的模式。
公司变大了,就不止 2 个员工了。可能四五百号,随机交给一个员工,不现实。中层管理出现。中层出现意味着我们公司的类也要进行改变,公司需要改革。
我们先实现一个改革之后的 Workshop 公司类。
class Workshop def initialize(count, master_name) @worker_count = count @workers = @worker_count.times.map do |i| Worker.new(i) end @master = Master.new(@workers) # 新增角色 end def <<(job) if job == :done @workers.map {|m| m << job} else @master.assign(job) # master 分配任务 end end def join @workers.map {|m| m.join} end end
可以看到,我们在初始化函数里新增了 @master
他接受 @workers
作为参数。毕竟领导要点兵啊。
<<
方法也进行了改进,由以前的 直接让 @workers 接收任务,变成 @master.assign
分配任务。
让我们来看下 Master 类
class Master def initialize(workers) @workers = workers end def assign(job) @workers.sort{|a,b| a.size <=> b.size}.first << job end end
其实也不复杂。我们保持了 @workers 的指针, assign
方法更像是把以前分配的逻辑接过来实现了一遍。
这次我们改了分配任务的方式,我们要根据 Worker#size
忙碌程度来分配任务。
毕竟嘛,领导有个方法论,会比小作坊高级很多。
一个领导就足够了么?不。
现实中我们见过形形色色的领导,有的是自己培养,有的是留过洋,有的是大厂空降。他们拥有不同的“方法论”,也就是 Master#assign
的方式可能不同。
我们给公司再加两个领导。
996ICU 领导:
我们使用了 Array#cycle
的方式,这是一个迭代器。比如 [1,2,3].cycle
每次 .next
会产生 1 、2 、3 、1 、2 、3 、1 、2 、3 .....
无限轮训。
这个方法论就是 996 方法论,只要干不死就往死里干。人海战术,把人轮番填上。
class ICU996Master def initialize(workers) @current_worker = workers.cycle # 迭代器 end def assign(job) @current_worker.next << job end end
等我们的公司变大了,我们的业务也会变得丰富,任务不是那么单一。很多工作要添加上组别 group_id ,分门别类的交给不同工种的打工人,比如 开发、产品、测试、设计、运营。
class GroupMaster GROUPS = [:group1, :group2, :group3] def initialize(workers) @workers = {} workers_per_group = workers.length / GROUPS.size workers.each_slice(workers_per_group).each_with_index do |slice, index| group_id = GROUPS[index] @workers[group_id] = slice end end def assign(job) worker = @workers[job.group].sort_by(&:size).first worker << job end end
然后我们可以把不同风格的领导班子集中起来
Masters = { normal: NormalMaster, ICU996: ICU996Master, group: GroupMaster }
我们改造下 Workshop
毕竟这个词是一个 工作室的意思,其实是个小部门。
我们改造之后,我们的小部门可以按照风格不同的领导进行分派工作。
class Workshop def initialize(count, master_name) # 新增 master_name 指定 @worker_count = count @workers = @worker_count.times.map do |i| Worker.new(i) end # 匹配 master @master = Masters[master_name].new(@workers) end def <<(job) if job == :done @workers.map {|m| m << job} else @master.assign(job) end end def join @workers.map {|m| m.join} end end
我们来看看不同部门的 TDD
it "check Workshop@ normal master" do ws = Workshop.new(4, :normal) finished = [] ws << lambda { puts "job1"; finished.push "job1"} ws << lambda { puts "job2"; finished.push "job2"} ws << lambda { puts "job3"; finished.push "job3"} ws << lambda { puts "job4"; finished.push "job4"} ws << :done ws.join assert_equal finished.size, 4 end it "check Workshop@ ICU996 master" do ws = Workshop.new(4, :ICU996) finished = [] ws << lambda { puts "job1"; finished.push "job1"} ws << lambda { puts "job2"; finished.push "job2"} ws << lambda { puts "job3"; finished.push "job3"} ws << lambda { puts "job4"; finished.push "job4"} ws << :done ws.join assert_equal finished.size, 4 end it "check Workshop@ group master" do ws = Workshop.new(4, :group) class GroupJob def initialize(group_id, &b) @group_id = group_id @blk = b end # 任务分组 def group "group#{@group_id}".to_sym end def call @blk.call(@group_id) end end finished = [] ws << GroupJob.new(1) { |group_id| finished.push(group_id)} ws << GroupJob.new(2) { |group_id| finished.push(group_id)} ws << GroupJob.new(3) { |group_id| finished.push(group_id)} ws << GroupJob.new(1) { |group_id| finished.push(group_id)} ws << :done ws.join assert_equal finished.size, 4 end
好吧,戏说不是胡说,改编不是乱编。
我们从现实的故事中走出来。
其实在这里 Master 类,可能会被叫做 Scheduler
即调度器。内部的方法主要是使用不同的策略来分配任务。
而不同的 Master 实现的 assign 方法就是 调度策略。
Workshop 其实 持有 @workers
,也就是说汇聚了实际工作线程的对象。他们可能会有另一个名字 —— 线程池( Thread Pool)
故事讲完了,你有没有学会呢? :D
大家好,我是 MARK24 。可以叫我 MARK 。这是我研究 Sinatra 的笔记。
阅读过程大约 10 分钟。
基于 Sinatra 2.1.0 进行讨论
set 系统可以让 Sinatra 在自身自由的定义 设置相关的变量。
比如定义模板所在:
set :views, settings.root + '/templates'
定义 session secret:
set :session_secret, ENV.fetch('SESSION_SECRET') { SecureRandom.hex(64) }
等等,非常自由且灵活。
set 系统这部分的源码恰巧是可以简单修改之后独立工作的。摘要如下:
# https://github.com/Mark24Code/sinatra-code-review/blob/master/lib/sinatra/base.rb#L1267 def define_singleton(name, cOntent= Proc.new) singleton_class.class_eval do undef_method(name) if method_defined? name String === content ? class_eval("def #{name}() #{content}; end") : define_method(name, &content) end end def set(option, value = (not_set = true), ignore_setter = false, &block) raise ArgumentError if block and !not_set value, not_set = block, false if block if not_set raise ArgumentError unless option.respond_to?(:each) option.each { |k,v| set(k, v) } return self end if respond_to?("#{option}=") and not ignore_setter return __send__("#{option}=", value) end setter = proc { |val| set option, val, true } getter = proc { value } case value when Proc getter = value when Symbol, Integer, FalseClass, TrueClass, NilClass getter = value.inspect when Hash setter = proc do |val| val = value.merge val if Hash === val set option, val, true end end define_singleton("#{option}=", setter) define_singleton(option, getter) # 原始代码放在一个类中, 如果我们想放在单文件执行,需要 改写为 `self.class.method_defined?` 调用到方法 # define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?" define_singleton("#{option}?", "!!#{option}") unless self.class.method_defined? "#{option}?" self end
Sinatra 内部实现了一套 配置系统,基于一个 DSL 语法 set 。 这是 Sinatra Class 部分初始化之后唯一的初始化的 DSL 。Sinatra 没有做很多复杂的前置工作。
一致很让我疑惑的是,这里的 getter 、setter 。我们传统理解的 是这样工作的:
class Sample def initialize() @name end # getter def name @name end # setter def name=(new_name) @name = new_name end end
但是 Sinatra 这里似乎是一个循环一样的,你会发现他的 setter 永远在调用 set 这是为什么呢?我一度非常迷惑。
setter = proc { |val| set option, val, true }
我刚开始进入这段是百思不得其解。但事实证明我格局小了。这部分其实根本不是传统的 setter, 我们观察传统的 setter 他的问题是必须要以一个实例变量为依托。所以他才必须写成这样。 如果是下面这样呢?
class Sample # getter def name "new value" end # setter def name=(new_name) # setter 的逻辑,就是覆盖式定义一个 新的 直接返回新值的 getter re_define_name_getter(new_name) end end
直接伪代码,我们每次调用 setter ,setter 的任务不是去修改一个 中间值,而是每次去重新定义 新的 getter 方法,定义的时候就塞入新的值。
这样依然保持了 getter 的功能!豁然开朗!
Sinatra 的 set 系统就是这样工作的,不论是 set 函数定义本身,还是 set 内部调用的 set ,还是用户最终在外部书写 set xxx, new_value
最终殊途同归的进入 set option, value, true
然后都会走到最后一部分,重新定义三个方法。
define_singleton("#{option}=", setter) define_singleton(option, getter) define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?"
setter 方法的作用就是调用 set 自身,这样只要被调用,时间上可以完成了一种循环。闭环调用(原谅我用了闭环这个词 :P ) getter 方法 是以新的值直接返回,respond_to 方法同理,以新值计算返回。
1.定义处有趣的写法 value = (not_set = true)
def set(option, value = (not_set = true), ignore_setter = false, &block) # .... end
可以通过实验证实这种写法的特点是:
如果 value 赋值 比如是 99 ,那么 value = 99, not_set = nil
如果 value 没有赋值, 那么 value = not_set = true
这里主要是没有赋值,not_set 开始发挥逻辑上的作用。
]]>这本书绝版了
]]>/etc
下面信息。比如系统登录用户,可以做一个系统粘合性较高的程序来使用。eof?
是一个外部迭代器可以用的方法,外部迭代器更方便控制。其他eof?
同理。未完待续
]]>最先发在 Ruby 节点,因为例子里主要举了 Ruby
由于 GIL 的问题,对于 Python 也有同样的意义,所以发在了 Python 节点,看的人多一点
]]>如果你想灵活的开展工作,又觉得 Rails 过于庞大(比如 Rails6+ 携带一个 Node )、文档要读很久,正在犹豫当中。
你恰巧知道 Sinatra 的存在,15 分钟读完的 Sinatra/README
又觉得自己行了,可是 Sinatra 似乎太简单了,你想要是 Sinatra 有 MVC 和开箱即用的 ORM 就好了。
这是最近做一个简单后端项目的沉淀,可以作为一个简单的起点。
一切基于 Sinatra+Rack,用一些胶水代码 把 Rack/Sinatra + 配置 + 文件目录 联系在一起开始工作。容易更改,简单明了。
分享一下,可能不够成熟,欢迎碰撞,让我可以学习更多~
github: https://github.com/Mark24Code/sinatra-app-template
geeknote: https://geeknote.net/mark24/posts/283
RubyChina: https://ruby-china.org/topics/41685
Lightweight web framework codebase. Just clone and develop on it.
Tech component: Rack+Sinatra+Sequel and default use Postgresql database.
Add rails-like migration command line helpers.
rake
or rake -T
rake server:run
APP_ENV=production bundle exec rake server:run
you can also use docker
docker built -t <what your docker image label> .
You can use DSL to config Key:Value
, then you application just use.
Config::Default.configure do set :app_env, ENV.fetch('APP_ENV'){ 'development' } set :bind, ENV.fetch('HOST') { '0.0.0.0' } set :port, ENV.fetch('PORT') { 3000 } set :secrets, ENV.fetch('SECRETS') { 'YOU CANNOT GUESS ME' } set :max_threads, ENV.fetch('MAX_THREADS') { 5 } set :database_url, ENV['DATABASE_URL'] end Config::Development.configure do set :database_url, 'ENV['DATABASE_URL']' end Config::Test.configure do set :database_url, ENV['DATABASE_URL'] end Config::Production.configure do # set :database_url, ENV['DATABASE_URL'] end
They have an inheritance relationship
Development < Default Test < Default Production < Default
In your code, just use Config
directly. core/bootstrap
do a work that loaded all necessery mods before your code.
Config.current # current env configuration Config::Development.database_url Config::Development Config::Development.database_url
You can also create your own Config
for your single Application:
class MyConfig < Config::Base end MyConfig.configure do # set :database_url, ENV['DATABASE_URL'] end
Edit config.ru
Lark also is Rack application. We can use Rack middlewares.
require_relative './cores/bootstrap' Bootstrap.rack # you can load Rack middleware here # mount applications require 'controllers/root_controller' # routers(handy config) map '/' do run RootController end
bases
directory are use for Application Base Class.
You can make different Configured Sinatra Application class here, then your application/controller just inherit the Base Class to create Application.
It will share Config, and make less code.
# Sinatra Doc http://sinatrarb.com/intro.html require 'sinatra/base' require 'json' class BaseController < Sinatra::Base # Inject config # Config & register Sinatra Extensions # Rewrite Views dir settings.views = File.expand_path(File.join($PROJECT_DIR, 'views')) configure :development do require 'sinatra/reloader' register Sinatra::Reloader end # mount Sinatra Helpers # mount Sinatra middlewares end # Share Configuration class MyPageServer < BaseController end class MyApiServer < BaseController end
Provide rails-like rake task help you build app quickly.
rake db:check # Checking for current migrations rake db:connect # Connect database rake db:console # Database Console rake db:create[database_name] # Create database rake db:create_migration[name] # Create a migration rake db:drop[database_name] # Drop database rake db:ls # List database tables rake db:migrate[version] # Run migrations rake db:rollback[version] # Rollback to migration rake db:version # Prints current schema version rake list # List all tasks rake seed:all # Seed: run all seeds rake seed:run[seed_name] # Seed: run seed rake server:run # Run server rake test # Run tests
. ├── Dockerfile # Common Dockerfile ├── Gemfile ├── Gemfile.lock ├── README.md ├── Rakefile # Rake Task Index File. ├── bases # Base configured class. You can make different BaseClasses then reuse them. │ └── base_controller.rb # You contoller can inherit it or write yourself. ├── config.ru # Application index. You can mount controllers and routes here. ├── configs # You can make different configs for applications │ └── config.rb # Base config ├── controllers │ └── root_controller.rb ├── cores # Inject ENVS and autoloads files, make MVC works │ ├── 01_config.rb # Names can controller mount order │ └── bootstrap.rb ├── dbs # You can make multi database here │ ├── default_db.rb # default database connect instance │ └── migrations # save database migrations ├── docs │ └── good.feature ├── log # Directory for save logs by default │ └── development.log ├── loggers # Loggers for application │ └── default_logger.rb ├── public # Public resources │ └── favicon.svg ├── seeds # Seeds ├── tasks # Rake helpful tasks │ ├── db_task.rb │ ├── seed_task.rb │ ├── server_task.rb │ └── test_task.rb ├── tests # Test cases │ └── test_demo.rb └── views # views template ├── base.erb └── root.erb
require_relative './cores/bootstrap' Bootstrap.rake
It will auto load files make sure rake task can work.
In rake we can use Config.current
to read configuration.
DB
also available.
In the same way
require_relative './cores/bootstrap' Bootstrap.rack # OR # Bootstrap.apps
It will autoload all dep mods. Share with a context.
cores/bootstrap.rb
defines different load orders, you can change.
In anther way, you can change filename to e.g 00_before_all.rb
、01_first_load.rb
to control mods load order.
最新的修改会更新在 BLOG
《 Ruby 元编程(第二版)》 5.4 节 单件类 在 Page125 这页,讲了一种情况:
class C def a_method 'C#a_method()' end end class C class << self def a_class_method '#C.a_class_method() #singleton' end end end class D < C;end obj = D.new D.a_class_method # => '#C.a_class_method() #singleton'
D.a_class_method 他是如何查找的呢?
本文就是寻找这个的答案。讲的是 Ruby 的方法查找再往前走一步。
为了说明这个问题,先要啰嗦的做一些铺垫。
下文中,此书简称为《元编程》
这张图实在总结的太美丽了。先放在这里。图片的出处,可以参考文末。
《元编程》里面里面总结了 Ruby 的查找规则:
“向右一步,然后向上查找”。
意思就是,向右寻找他的父,然后开始往上寻找继承关系,通过这种方式查找方法。
比如以下代码
class C def a_method 'C#a_method()' end end class D < C;end obj = D.new obj.a_method
obj 如何查找 a_method 方法呢?
如果我们给 obj 对象添加单例类,他会如何查找呢?
class C def a_method 'C#a_method()' end end class D < C;end obj = D.new # 定义单例类 class << obj def a_singleton_method "obj#a_singleton_method" end end obj.a_singleton_method
obj.a_singleton_method 会如何查找方法呢?
可以通过一下方式检验
obj.singleton_class.superclass # => D
他会按照如图的方式,其实实例对象创造了一个 单例类 可以标记为 #obj,用#表示单例类。 #obj 会出现在对象和真正的类中间。
我们也能用上面
“向右一步,然后向上查找”。
来指导我们查找,只不过对象存在一个单例类罢了。
但是问题来了,回到文章的最开头。
class C def a_method 'C#a_method()' end end class C class << self def a_class_method '#C.a_class_method() #singleton' end end end class D < C;end obj = D.new D.a_class_method # => '#C.a_class_method() #singleton'
这个例子。在类 C 上定义了单例方法,并且我们指导所有东西在 Ruby 里都是对象,都可以定义单例方法。
这就是文章开头最先的图片。所有的类都可以定义单例类。这种情况下,D.a_class_method 应该如何查找呢?
“向右一步,然后向上查找”。
似乎帮不了我们了。因为我们面临一个问题,让我来描述下:
我们把 D 当做一个对象,开始寻找他的方法。
拿这幅图做例子:
Dog 开始寻找定义的方法,向右一步,进入自己的 单例类 #Dog,然后应该做什么,选择向上么?是走 他的父类 Class,还是 应该往 单例类的继承链往上找呢?
《元编程》文末的几句话,似乎在暗示黄色这条线的寻找方向,但是作者并没有真正说清楚:
我先放出答案,如下图所示:
对象的方法,遵循
“向右一步,然后向上查找”。
类方法的查找是我们关心的,可以看到实际结果是,它沿着继承的单例类一路向上,然后再进入父类。
寻找这个答案的过程中,我看了挺多资料和文字,还有问一些 Ruby 方面的朋友都没有真正分析到这一步。
我最后是怎么找到答案的呢? 这就得借助 Ruby 自身完善的自省机制。(吐槽,其他语言可能都没有实现的那么细致)。
其实 Ruby 自身的很多属性都绑定在自身了,直接向 Ruby 问答案就好了
class C def a_method 'C#a_method()' end end class C class << self def a_class_method '#C.a_class_method() #singleton' end end end class D < C;end obj = D.new D.a_class_method # => '#C.a_class_method() #singleton'
我们知道 obj.ancestors 可以打印继承关系,但是这个很遗憾的是它不会打印 单例类。
单例类实际上是一个隐藏的存在。这也就是研究这个问题很难得地方,因为隐藏,似乎只能通过源码和外部资料去查看。
实际上我们是可以拿到 obj.singleton_class 的,然后我们前面分析了一些结论,大致给出了一个对象的继承模型。
obj.singleton_class.ancestors # => [#<Class:#<D:0x00007feae092efc8>>, D, C, Object, Kernel, BasicObject]
就可以打印出,对象查找的顺序。
同理,我们想要知道 D 的方法的查找顺序
D.singleton_class.ancestors # => [#<Class:D>, #<Class:C>, #<Class:Object>, #<Class:BasicObject>, Class, Module, Object, Kernel, BasicObject]
这个其实就是 D 查找方法的顺序,可以看到,他先是把所有的单例类走了一遍,然后开始进入自己的父。
最后,这句话
“向右一步,然后向上查找”。
有了新内涵, 向右一步的过程中,优先的走单例类(如果有的话)以及单例的继承,结束后,开始进入自己真正的父,即向上在继承关系中寻找。
单例类也可以看成是一种外挂方法(比喻不严谨但是很好理解),先在外挂方法里找,也可以顺着外挂继承链找。找不到再到继承关系里面找。
有人可能会问,为啥继承体系要搞得那么复杂?
借用 《元编程》里面的一句
这样你就可以在 D 中 调用 C 的方法了。
把对象穿成链表,然后相当于你可以拥有和复用这个链条上所有的方法。
元编程的一部分思想也就是动态的修改、创造、转发方法。还有 《元编程》里面提到的 “自由方法”我的理解就像是把继承链中某些方法复制,然后粘贴到当前对象执行,在继承链上跳跃执行方法……
这一切都是为了极大地自由。
我以前一致不太理解“Ruby 是快乐优先”这句话是什么意思,现在我的理解——这种快乐就是自由,拥有自由的快乐。
图片来自文章
安利一波作者图片配色
书籍推荐 《 Ruby 元编程(第 2 版)》
有更好的方法可以告诉我,我最新在学习 Ruby
最新的修改会更新在 BLOG
]]>这背后的动力一部分是被景仰和尊重的心理需要(别人都不会用的技术我是大牛),另一部分则是对某项技术的社群形成了一个图像,希望别人用这个图像来描述自己和认同自己。
虽然这种心态也让我获得了一些有趣和有用的经验,比如 Emacs,但总地说来是让我浪费了太多的时间在过各种 tutorial 和写 hello world 上。Language hopping 的结果就是始终无法深入钻研一个东西,也就不知道它能干什么,它的局限在哪里。
这样看来,Emacs 的厉害之处在于“从入门到精通”(精通是个境界,这辈子都不可能精通的)提供了一条连续而无穷尽的学习途径,只要是和文本有关的问题,都可以尝试着用它去解决,并且在这个过程中可能又学到了一点新东西。Python 也是类似,入门门槛低,天花板几乎无限高,但中间每一层都可以探索。
而 Ruby 的问题大概就是这么多年 Rails 独领风骚,基本上要求学习者写完 hello world 和 fizzbuzz 之后就开始研究元编程、DSL 和 Rails 的各种令人眼花缭乱的实现技巧,缺乏更广泛的问题域和连续的学习路径。
作为比较新的语言,Racket 和 Roku 也有类似的问题(不知道为什么我对 R 开头的语言比较感兴趣)。不过我还是想学好 Ruby 去给 DHH 打工的,尽管只会 fizzbuzz (哭
]]>包括有善的压测
论坛没啥内容
程序猿也不是目标用户
所以别认为是推广就行
远程 cloudflare nginx frps
本地 frpc discourse
测试阶段
只求能用 速度就别指望了太快
中文分词还是没完善
导致搜索和敏感词过滤效果一般
其他问题慢慢发现解决
先谢啦
discourse 的问题也可以问我
]]>一个比较当前提交和上次提交修改的代码的覆盖率的例子:
$ incrcov HEAD^ HEAD +----------------+-----------+-------------+---------------+---------------+--------------+ | Path | Method | Total Lines | Covered Lines | Coverage Rate | Missed Lines | +----------------+-----------+-------------+---------------+---------------+--------------+ | app/demo2.rb:6 | say_world | 2 | 1 | 50.0% | 7 | +----------------+-----------+-------------+---------------+---------------+--------------+ Overall incremental test coverage: 75.0% Number of updated methods: 2 Number of low test coverage(<90%) methods: 1
]]>使用 sinatra 框架开发的。
github 在 https://github.com/weeklyreportinfo/weeklyreportinfo
]]>“ WITH DEVKIT ”是啥意思啊?
]]>nums.sort! 明显要比 nums = nums.sort 要慢 10ms
这是为什么呢? sort!的操作更多?虽然 sort!写起来格外的方便,但是吃了很多时间的亏。
]]>我在悉尼, Ruby On Rails Backend 开发。
有兴趣的可以一起交流下。
]]>a = "hi" def a.you "hi, you" end a.you # => hi, you "hello".you # => undefined method 'you'
上面的 a.you 方法就是 a 这个字符串实例的 Singleton Method 。
同样如果拿我们最熟悉的类举例子的话就是:
class A def self.hi 'hi A' end end A.hi # => hi A
上面的定义方法所有的同学应该都知道,其实就是类方法的定义,在 Ruby 中类方法其实就是类的 Singleton Method ,就像上面说的因为 A 也是 Class 这个类的一个实例(通过 A.class 就可以知道),所以,其实上面的方法也可以这样定义:
class A end def A.hi 'hi A' end A.hi # => hi A
这样的话,就和上面字符串 a 的效果一样,就更容理解了。 所以关键是理解我们所定义的类也是 Class 这个类的一个实例,这是关键。
其实我想说的,下面才是关键,
蛋人网 http://eggman.tv 的最新系列大课程<Ruby 元编程="">已经开坑了,从今天开始会陆续更新,第一节视频课件<Ruby 元编程的介绍和使用场景="">已经放出,免费的, http://eggman.tv/c/s-ruby-meta-programming ,计划每周更新两期,上面讲述的是课件一部分内容,我们计划会在整个课件中为大家讲述:
等等众多深入内容的讲解,当然还包括众多的实例演示和如何使用这些高级技巧,欢迎各位大哥大嫂前来支持。
]]>那么 ruby 的主流版本呢,主要是主流库啊,工具什么的所用的版本
]]>r = (1..99999999999999).to_a
本来以为很快就能把内存占到 2G 来着,结果才 500M 就报错了
(有三个进程,是我后来又开了俩,但是这次 700M 才报错)
所以现在问题来了,这到底啥情况?
]]>地址:github
顺便增加了更新 stations.dat 的功能
不是很会玩 rubygems ,就不考虑上传什么的了
话说,编码问题还是很麻烦啊=-= 在 windows 上 ARGV 里默认是 GBK 编码,我在 ssh 里用 arch 时 ARGV 是 ASCII_8BIT,在电脑上的 arch 上是 utf-8 , ARGV 的内容默认还是 frozen 的。 Rubinius/JRuby 什么的还木有测试
求 star 啊求 star,顺便如果那里写的不好的话求指点。
]]>按照 readme 安装到我的 Mac 上,使用他给的 admin 帐号登录,结果 进不去 admin 的面板
求解答,有铜币送上~~
]]>现在,喜欢 Ruby 的同学终于可以优雅地使用 OSS 了。先看看 Rubyist 如何上传和下载文件吧:
require 'aliyun/oss' bucket = Aliyun::OSS::Client.new( endpoint: 'http://oss-cn-hangzhou.aliyuncs.com', access_key_id: 'xxx', access_key_secret: 'yyy').get_bucket('bucket') bucket.put_object('ruby') { |s| s << 'hello world' } bucket.get_object('ruby') { |c| puts c } bucket.put_object('rails', :file => '/tmp/x') bucket.get_object('rails', :file => '/tmp/y')
是不是 so easy ? OSS SDK for Ruby 支持 OSS 目前绝大部分功能,主要的 highlights 包括:
下面我们随便挑几个来聊一聊:
OSS 用户在一个 bucket 下可能有成千上万的 objects ,所以 OSS 不会将 object 列表一次性全部返回,每次最多返回 1000 条。如果我要列出前 1001 个 object 怎么办?一般来说用户可能要调用两次接口。但是在 ruby 中你只需要一行:
bucket.list_objects.take(1001)
再比如用户要找 files/下文件名包含'ruby'的文件:
bucket.list_objects(prefix: 'files/').find { |x| x.key.include?('ruby') }
是不是非常方便?
流式上传允许用户动态地一边生成内容,一边上传到 OSS ,上传的数据可以 generated on the fly 。如下面的例子:
bucket.put_object('numbers') do |stream| (1..1_000_000).each { |i| stream << i << "\n" } end
注意:这里并不是要 100 万个数字都生成完后再发送,而是一边生成一边发送的。
要做到上面的效果并不容易,试想一下,put_object
要如何调用所接受的 block 参数?如果调用那么 100 万个数字都一口气生成完了,所以,如何能做到对一个函数调用一半? 答案就是使用Fiber
。
def hello puts 'hello' Fiber.yield puts 'world' end def world fiber = Fiber.new { hello } puts 'first' fiber.resume # puts 'hello' puts 'second' fiber.resume # puts 'world' end world
类似上面的代码,stream#<<
内每接受一部分内容,会将自己 yield 出去,这样已经接受的内容就可以立即发送出去。有兴趣可以查看SDK 代码。
在上传大文件时如果中途失败了,要重新上传是不是很沮丧?有了断点上传,中途失败后可以接着上次的进度继续上传。在 ruby 中只需要:
bucket.resumable_upload('object_key', 'local_file')
断点上传依赖于 OSS 提供的 multipart 功能,类似于一个事务:
只有最后一步成功后文件才算上传成功,在此之前文件对用户是不可见的。
文件被中断后如何恢复上传?如何知道哪些 parts 已经上传成功?这需要记录事务的状态信息。 SDK 的做法是将这些信息(称为 checkpoint )保存在一个本地的 json 文件中。每上传完一个 part 就更新一次 checkpoint 。恢复上传时从 checkpoint 文件中恢复上传的进度。
另外断点上传 /下载中也利用多线程实现加速,先看结果:
$ruby tests/test_large_file.rb -n test_large_file_1gb Run options: -n test_large_file_1gb --seed 7587 # Running: user system total real Upload with put_object: 20.810000 1.880000 22.690000 ( 62.843336) Upload with resumable_upload: 28.720000 9.740000 38.460000 ( 33.963555) Download with get_object: 17.300000 4.550000 21.850000 ( 47.132476) Download with resumable_download: 23.260000 9.530000 32.790000 ( 31.883211)
Ruby 或者 Python 的多线程一直是个“迷”,但是对于这种 IO 多场景,用多线程效果还是很明显的。因为进行 IO 的标准库函数在需要等待 IO 时会将当前线程切出去。参考: http://yehudakatz.com/2010/08/14/threads-in-ruby-enough-already/
借助 rails ,可以在 15 分钟内搭建一个oss 文件管理器,可以查看 /上传 /下载文件,效果图如下: