☰
concurrent-ruby ErlangActor 实战指南:基于 Erlang 语义的 Ruby 并发 Actor 编程
2026/10/12 3:40:56 网站建设 项目流程
  • 开发工具

【免费下载链接】concurrent-ruby

Modern concurrency tools including agents, futures, promises, thread pools, supervisors, and more. Inspired by Erlang, Clojure, Scala, Go, Java, JavaScript, and classic concurrency patterns.

项目地址:https://gitcode.com/gh_mirrors/co/concurrent-ruby
点击查看免费下载

concurrent-ruby 的concurrent-ruby-edge扩展包提供了一套名为Concurrent::ErlangActor的 Actor 并发原语,它在行为语义上对标 Erlang 进程:支持消息收发(tell/ask/receive)、内部状态维护、linking、monitoring、trap_exit、exit 信号传播等经典能力。本文以 erlang_actor.out.md 为主线,结合 erlang_actor.rb 源码与 erlang_actor_spec.rb 测试,完整演示两种 Actor 类型(:on_thread与:on_pool)的写法差异、消息匹配与分发规则、Erlang 风格的错误处理与监控,以及基于Promises的生命周期集成,帮助你直接用 Ruby 构建出具备 Erlang 容错哲学的并发系统。

一、背景:为什么需要 ErlangActor

在 README.md 的 Edge Features 一节中,concurrent-ruby-edge被定位为「正在积极开发、接口可能频繁变化、不保证向后兼容」的实验性功能集,成熟后会逐步并入concurrent-ruby主库。ErlangActor正是其中的一员,它依托于全新的Promises(Future/Promise)框架,将 Erlang 进程模型移植到 Ruby。

Erlang 进程模型的三个核心特性在 ErlangActor 中一一对应:

Erlang 概念ErlangActor 对应实现
进程间异步消息传递Pid#tell/Pid#ask
选择性接收receiveEnvironment#receive(支持类型/谓词匹配)
spawn_link/monitor/process_flag(trap_exit, true)spawn(link: true)/monitor/trap
exit/1terminate(为避免与Kernel#exit冲突而改名)

使用时通过concurrent-edge入口加载:

require 'concurrent-edge'

在 concurrent-edge.rb 中可以看到它统一加载了concurrent/edge/channel、concurrent/edge/processing_actor与concurrent/edge/erlang_actor等模块——ErlangActor 的 mailbox 正是建立在Promises::Channel之上的(详见 channel.rb)。

二、快速上手:把 Actor 当作一次异步执行

最简单的用法是把 Actor 当作异步执行单元。不过正如文档所提示,若只是「算一次」,Promises.future { 1 + 1 }更合适;Actor 的价值在于有状态、可持续收消息。

actor = Concurrent::ErlangActor.spawn(type: :on_thread, name: 'addition') { 1 + 1 } # => #<Concurrent::ErlangActor::Pid:0x000002 addition running> actor.terminated.value! # => 2

这里spawn返回的是Pid(进程标识)。从源码看,Pid 是「可以存储和传递的 Actor 公共引用」,Actor 的其它内部对象不会对外暴露;terminated返回一个Promises::Future,Actor 正常结束时以最终值为其结果。Pid#to_s会根据terminated的当前状态渲染出running、terminated normally with ...或terminated because of ...(erlang_actor.rb),这也是示例中 Pid 字符串输出的来源。

三、维护内部状态:tell / ask / reply 的消息循环

Actor 最典型的场景是「收消息、改状态、回答案」。下面的sumActor 维护一个内部累加变量,直到收到:done才退出:

actor = Concurrent::ErlangActor.spawn(type: :on_thread, name: 'sum') do sum = 0 # internal state # receive and sum the messages until the actor gets :done while true message = receive break if message == :done # if the message is asked and not only told, # reply with the current sum (has no effect if actor was not asked) reply sum += message end # The final value of the actor sum end # => #<Concurrent::ErlangActor::Pid:0x000003 sum running>

与外部交互的方式有两种:

  • tell(异步告知):立即返回 Actor 自身,不等待处理结果;
  • ask(同步询问):阻塞当前线程直到 Actor 调用reply返回答案。
# tell returns immediately returning the actor actor.tell(1).tell(1) # => #<Concurrent::ErlangActor::Pid:0x000003 sum running> # blocks, waiting for the answer actor.ask 10 # => 12 # stop the actor actor.tell :done # => #<Concurrent::ErlangActor::Pid:0x000003 sum running> # The final value of the actor actor.terminated.value! # => 12

注意tell支持链式调用(返回self);ask会把消息包装成内部的Ask信号(携带一个ResolvableFuture作为探针)投进 mailbox,reply本质上是reply_resolution true, value, nil的快捷方式(erlang_actor.rb),即把答案 resolve 到那个 future 上。如果 Actor 收到的是tell而非ask,reply不会有任何效果——对应源码中reply_resolution首先检查@reply是否为空的逻辑(erlang_actor.rb)。

源码还保证了ask永远不会被「晾死」:Actor 不调用reply会抛NoReply,Actor 已终止会抛NoActor(erlang_actor.rb),对应测试见 erlang_actor_spec.rb。

四、两种 Actor 类型::on_thread与:on_pool

Actor 类型在spawn的第一个参数位置指定:type: :on_thread或type: :on_pool。二者的核心差异在于receive的返回方式。

:on_thread:阻塞式

receive会阻塞 Actor 所占据的线程,直到有消息可用,然后返回消息(或先调用给定的 block)。写法最直观:

ping = Concurrent::ErlangActor.spawn(type: :on_thread) { reply receive } # => #<Concurrent::ErlangActor::Pid:0x000004 running> ping.ask 42 # => 42

执行流程:先调用receive阻塞线程 → 收到消息后作为参数传给reply→ 把答案回给ask→ Actor 主体执行完毕,正常终止。

从源码看,OnThread 的run会Thread.new创建一条新线程执行主体,receive则在循环里调用@Mailbox.pop_matching(matcher, timeout, TIMEOUT)阻塞取消息(erlang_actor.rb)。

局限:每个:on_threadActor 独占一个 Ruby 线程。线程的创建耗时耗资源,且 Ruby 进程能创建的线程数量有限,可能因触顶而创建失败。若只是少量 Actor,:on_thread足够。

:on_pool:回调续体式

:on_poolActor 运行在线程池上,receive时必须把线程归还给池子,因此调用receive会结束当前作用域的执行;需要把后续处理写成 block(续体),当消息可用时再被调用:

ping = Concurrent::ErlangActor.spawn(type: :on_pool) { receive { |m| reply m } } # => #<Concurrent::ErlangActor::Pid:0x000005 running> ping.ask 42 # => 42

流程:receive记住传入的 block 并停止当前作用域 → 消息到达后,block 收到消息被调用 → block 的返回值即该 Actor 正常终止时的最终值。

优势::on_poolActor 运行在线程池上,创建更快更廉价,不新建线程,因此可创建数量几乎不受线程数限制(只受内存限制)。经验法则:只创建少量 Actor 用:on_thread;要创建成百上千个或大量短生命周期 Actor 时用:on_pool。

从源码看,OnPool 的receive会记录规则后throw JUMP, [RECEIVE]结束当前作用域(erlang_actor.rb),随后通过Promises.future_on(@Executor, ...)在指定执行器上调度,并用internal_receive基于Channel#pop_op_matching异步等待消息(erlang_actor.rb)——这与:on_thread的阻塞式pop_matching形成鲜明对比。

五、接收消息:receive 的完整形态

5.1 最简形式

:on_thread下直接receive取回下一条消息:

actor = Concurrent::ErlangActor.spawn(type: :on_thread) { receive } # => #<Concurrent::ErlangActor::Pid:0x000006 running> actor.tell :m # => #<Concurrent::ErlangActor::Pid:0x000006 running> actor.terminated.value! # => :m

:on_pool也支持,因为未给 block 时默认使用{ |v| v }:

actor = Concurrent::ErlangActor.spawn(type: :on_pool) { receive { |v| v } } # => #<Concurrent::ErlangActor::Pid:0x000007 running> # can simply be following actor = Concurrent::ErlangActor.spawn(type: :on_pool) { receive } # => #<Concurrent::ErlangActor::Pid:0x000008 running> actor.tell :m # => #<Concurrent::ErlangActor::Pid:0x000008 running> actor.terminated.value! # => :m

这条「默认 block 为恒等函数」的逻辑实现在canonical_rules:block = given_block || -> v { v },且无规则时自动补一条on(ANY, &block)(erlang_actor.rb),测试见 erlang_actor_spec.rb。

5.2 限定消息类型

可以只接收特定类型的消息,不匹配的消息留在 mailbox 里(下面的'junk'被忽略):

Concurrent::ErlangActor. spawn(type: :on_thread) { receive(Numeric).succ }. tell('junk'). # ignored message tell(42). terminated.value! # => 43

:on_pool上类型限定必须给 block:

Concurrent::ErlangActor. spawn(type: :on_pool) { receive(Numeric) { |v| v.succ } }. tell('junk'). # ignored message tell(42). terminated.value! # => 43

顺带一提,为:on_pool写的带 block 形式在:on_thread上同样可用:

Concurrent::ErlangActor. spawn(type: :on_thread) { receive(Numeric) { |v| v.succ } }. tell('junk'). # ignored message tell(42). terminated.value! # => 43

这里receive的参数其实是「匹配器」:Numeric这类类对象直接用===匹配(Numeric === 42为真),所以可以传入任意实现了#===的对象作为模式。

5.3 超时与timeout_value

receive支持timeout:与timeout_value:参数:若在超时时间内没有匹配消息,则返回timeout_value;若提供了on(TIMEOUT)规则则调用其处理。测试中的典型组合(erlang_actor_spec.rb):

# 邮箱里只有 :v,等待 Numeric 超时 receive Numeric, timeout: 0, timeout_value: :timeout # => :timeout receive Numeric, timeout: 1 # 等 1 秒,:v 不匹配则超时返回 nil

源码里超时规则会被canonical_rules自动前置到规则列表首位,避免被ANY抢先匹配(erlang_actor.rb)。

5.4 多规则分发:基于消息内容路由

receive可以一次性给出多条on(matcher, ...)规则,按顺序匹配第一条命中的规则并执行对应 block:

actor = Concurrent::ErlangActor.spawn(type: :on_thread) do while true receive(on(Symbol) { |s| reply s.to_s }, on(And[Numeric, -> v { v >= 0 }]) { |v| reply v.succ }, # put last works as else on(ANY) do |v| reply :bad_message terminate [:bad_message, v] end) end end # => #<Concurrent::ErlangActor::Pid:0x000009 running> actor.ask 1 # => 2 actor.ask 2 # => 3 actor.ask :value # => "value" # this malformed message will terminate the actor actor.ask -1 # => :bad_message # the actor is no longer alive, so ask fails actor.ask "junk" rescue $! # => #<Concurrent::ErlangActor::NoActor: #<Concurrent::ErlangActor::Pid:0x000009 terminated because of [:bad_message, -1]>> actor.terminated.result # => [false, nil, [:bad_message, -1]]

这里演示了三种匹配器:

  • on(Symbol):只匹配 Symbol 消息;
  • on(And[Numeric, -> v { v >= 0 }]):And逻辑组合匹配器,所有子匹配器都命中才算命中;
  • on(ANY):兜底规则,放在最后充当else。ANY是一个===恒为true的单例对象,定义于 channel.rb。

And、Or等组合匹配器定义在 erlang_actor.rb 中:And[...]要求全部命中,Or[...]要求任一命中。注意And要求构造时所有 matcher 都能对值做===,所以-> v { v >= 0 }这种谓词 lambda 可以直接参与组合。

terminated.result返回[fulfilled, value, reason]三元组,这里为[false, nil, [:bad_message, -1]],即 Actor 以 reason[:bad_message, -1]非正常终止。

5.5 多规则 + 循环::on_pool的 body 递归写法

:on_poolActor 无法写while true(receive会结束作用域),因此用「反复调用自身方法」实现循环。文档给出的做法是定义环境模块:

module Behaviour def body receive(on(Symbol) do |s| reply s.to_s body # call again end, on(And[Numeric, -> v { v >= 0 }]) do |v| reply v.succ body # call again end, # put last works as else on(ANY) do |v| reply :bad_message terminate [:bad_message, v] end) end end # => :body actor = Concurrent::ErlangActor.spawn(type: :on_pool, environment: Behaviour) { body } # => #<Concurrent::ErlangActor::Pid:0x00000a running> actor.ask 1 # => 2 actor.ask 2 # => 3 actor.ask :value # => "value" # this malformed message will terminate the actor actor.ask -1 # => :bad_message # the actor is no longer alive, so ask fails actor.ask "junk" rescue $! # => #<Concurrent::ErlangActor::NoActor: #<Concurrent::ErlangActor::Pid:0x00000a terminated because of [:bad_message, -1]>> actor.terminated.result # => [false, nil, [:bad_message, -1]]

environment:参数接受两种对象:继承自Concurrent::ErlangActor::Environment的类,或任意 Module。传入 Module 时会被extend到一个新的 Environment 实例上;如果多个 Actor 共享同一模块,官方建议改用类以复用(erlang_actor.rb、erlang_actor.rb)。

测试里还有一种等价写法::on_pool未提供 body block 时使用默认的-> { start },因此可以定义一个带start方法的模块/类,把循环放在start里(erlang_actor_spec.rb):

definition_module = Module.new do def start @sum = 0 receive on(Numeric, &method(:count)), on(:done, &method(:stop)), on(TIMEOUT, &method(:fail)), keep: true, timeout: 0.1 end def count(message) reply @sum += message end def stop(_message) terminate :normal, value: @sum end def fail(_message) terminate :timeout end end actor = Concurrent::ErlangActor.spawn(type: :on_pool, environment: definition_module) actor.tell 1 actor.ask(2) # => 3

注意这里还演示了on(:done, &method(:stop))——匹配器可以是任意值对象(Symbol === :done为真)。

5.6keep: true:规则复用

若消息分发的规则集合保持不变,keep: true会保留当前receive规则,直到下一次receive调用将其替换。这样:on_poolActor 也无需显式递归:

actor = Concurrent::ErlangActor.spawn(type: :on_pool) do receive(on(Symbol) { |s| reply s.to_s }, on(And[Numeric, -> v { v >= 0 }]) { |v| reply v.succ }, # put last works as else on(ANY) do |v| reply :bad_message terminate [:bad_message, v] end, keep: true) end # => #<Concurrent::ErlangActor::Pid:0x00000b running> actor.ask 1 # => 2 actor.ask 2 # => 3 actor.ask :value # => "value" # this malformed message will terminate the actor actor.ask -1 # => :bad_message # the actor is no longer alive, so ask fails actor.ask "junk" rescue $! # => #<Concurrent::ErlangActor::NoActor: #<Concurrent::ErlangActor::Pid:0x00000b terminated because of [:bad_message, -1]>> actor.terminated.result # => [false, nil, [:bad_message, -1]]

keep的实现见 OnPool#receive(保存@keep_behaviour = keep),在apply_behaviour中当@keep_behaviour为假时才清空规则(erlang_actor.rb);测试用例见 erlang_actor_spec.rb。

六、Erlang 行为:linking、monitoring、trap 与 exit

ErlangActor 在行为上对标 Erlang 进程,支持链接(link)、监控(monitor)、退出信号(exit)等。方法命名与 Erlang 保持一致或近似,唯一的例外是把exit改名为terminate,以免与Kernel#exit冲突(erlang_actor.rb)。

actor = Concurrent::ErlangActor.spawn(type: :on_thread) do spawn(link: true) do # equivalent of spawn_link in Erlang terminate :err # equivalent of exit in Erlang end trap # equivalent of process_flag(trap_exit, true) receive end # => #<Concurrent::ErlangActor::Pid:0x00000c running> actor.terminated.value! # => #<Concurrent::ErlangActor::Terminated:0x00000d # @from= # #<Concurrent::ErlangActor::Pid:0x00000e terminated because of err>, # @reason=:err>

6.1 link / unlink

  • link(pid):与另一 Actor 建立链接。若对方不存在:未 trap 时抛NoActor;已 trap 时返回true但会把 reason 为noproc的退出信号送入调用方邮箱(erlang_actor.rb)。
  • unlink(pid):解除链接。已 trap 时邮箱里可能残留因该链接产生的Terminated消息,官方建议用receive on(And[Terminated, -> e { e.pid == pid }], true), timeout: 0清理(erlang_actor.rb)。
  • linked?(pid):查询是否已链接(测试见 erlang_actor_spec.rb)。
  • spawn(link: true):原子地创建并链接,等价 Erlang 的spawn_link(测试见 erlang_actor_spec.rb)。

链接的传播规则(对应 consume_exit):

  • 未 trap:收到非:normal的退出信号,Actor 以该 reason 退出,并把退出信号继续传播给它的链接 Actor;
  • 已 trap:退出信号被转换为Terminated消息放入邮箱,可作为普通消息接收;
  • reason 为:normal:不会导致对方退出,但 trap 时仍会转为Terminated消息。

测试对这一套规则覆盖非常细(erlang_actor_spec.rb),例如「链接子 Actor 抛异常时,未 trap 的父 Actor 也以该异常退出」。

6.2 monitor / demonitor

监控是单向的,监控方不随被监控方退出而退出:

  • monitor(pid):开始监控,返回一个Reference;被监控 Actor 死亡(或不存在)时,监控方收到Down消息(含from、reference、info)。重复 monitor 同一 pid 会生成多个相互独立的监控。
  • demonitor(reference, *options):停止监控,支持两个选项:
    • :flush:顺带从邮箱清掉(一条)对应的Down消息;
    • :info:返回true/false表示监控是否真的被找到并移除(若Down已入队则返回false)。
    • 组合使用:flush, :info时,若发生了 flush 则返回false,否则true。
  • monitoring?(reference):查询监控状态。

与 Erlang 一致,被监控的 Actor 终止时若监控者已先终止,则Down中的info统一为NoActor(保证确定性),见 after_termination 及测试 erlang_actor_spec.rb。

6.3 terminate(Erlang 的 exit)

terminate有两种调用形态(erlang_actor.rb):

  • 不传 pid:终止调用者自身,reason 作为退出原因;
  • 传 pid:向目标 Actor 发送退出信号。reason 的行为:
    • 普通对象(非:normal、:kill):目标未 trap 则以其退出;已 trap 则转为Terminated消息;
    • :normal:目标不退出,trap 时转为Terminated消息;
    • :kill:发送不可 trap的强杀信号,目标无条件以:killed退出。

:kill的不可 trap 语义、:kill与普通 reason 在 trap 下的差异均有测试佐证(erlang_actor_spec.rb)。

七、生命周期与错误模型

  • Pid#terminated:返回Promises::Future,携带 Actor 最终结果(正常值为返回值,异常为 reason)。调用terminated.value!会在非正常终止时抛出对应异常(如RuntimeError),terminated.result则返回[fulfilled, value, reason]。
  • Pid#ask的异常保证:Actor 未回复抛NoReply;Actor 已终止抛NoActor(NoActor#to_s会带上 Pid 与终止原因,这正是示例中#<Concurrent::ErlangActor::NoActor: ...>的来源)。
  • 内部信号:Link、UnLink、Monitor、DeMonitor、Ask、Kill、Terminate、DownSignal等均继承自私有类AbstractSignal(erlang_actor.rb),receive在取消息时会先经consume_signal处理信号、把纯业务消息返回给用户规则(erlang_actor.rb)。因此业务代码通常不需要感知这些内部信号。

八、Promises 集成与高级用法

8.1tell_op与ask_op

Pid还提供了基于Promises::Future的异步变体(erlang_actor.rb):

  • tell_op(message):返回一个以self(Pid)为结果的 future,消息入队即完成;
  • ask_op(message, probe = Promises.resolvable_future):返回以回复为结果的 future,可传入自定义probe。

ask_op的错误路径也是 future 式的:无回复时reason为NoReply,Actor 已终止时reason为NoActor(测试见 erlang_actor_spec.rb)。这让 Actor 可以无缝融入Promises的链式/组合编程。

8.2 有界 mailbox 与背压

spawn的channel:参数允许传入自定义Promises::Channel作为 mailbox。Channel 默认容量无限(UNLIMITED_CAPACITY,见 channel.rb),但可以用Channel.new(capacity)构造有界通道。此时:

  • tell(message, timeout):当 mailbox 已满时会阻塞当前线程,直到有空间或超时,返回true/false;不传 timeout 则返回self;
  • ask同理。

这正是构建**背压(backpressure)**系统的关键手段,也是原文档「待补充章节」中明确点出的方向。注意:Channel 可以与其它抽象共享,但 Actor 必须是唯一消费者,否则内部信号可能丢失(erlang_actor.rb)。

8.3 环境(environment)定义

spawn的environment:参数支持:

  • 继承Concurrent::ErlangActor::Environment的类(推荐多 Actor 复用时);
  • 任意 Module(会被extend到新 Environment 实例,多 Actor 复用时更耗资源)。

Environment 实例为 Actor 主体提供了receive、reply、reply_resolution、spawn、link、unlink、monitor、demonitor、trap、terminate、pid、name、terminated等全部方法(erlang_actor.rb)。此外还可以通过include Concurrent::ErlangActor::EnvironmentConstants直接使用ANY、TIMEOUT、And、Or等匹配常量,docs-source/erlang_actor.init.rb展示了这种写法。

九、实践建议

原文档以「待补充章节」形式留下了几条最佳实践,结合源码与测试可归纳为:

  1. 始终给receive设置超时:timeout:+timeout_value:或on(TIMEOUT)规则,消息未到达时做明确处理,不要让 Actor 卡死;ask也应传timeout与timeout_value(源码ask(message, timeout, timeout_value)在超时时返回timeout_value,见 erlang_actor.rb)。
  2. 对无法识别的消息兜底:用on(ANY)兜底——要么记录日志丢弃,要么更防御性地terminate(上文 5.4 的:bad_message终止即是后者)。
  3. 按规模选择 Actor 类型:少量 Actor 用:on_thread直白简单;大量或短命 Actor 用:on_pool避免线程耗尽。
  4. 善用链接/监控构建容错结构:spawn(link: true)快速失败传播、monitor单向观察、trap把退出信号转消息处理,组合出 supervisor 式的分层结构。
  5. 有界 mailbox 用于背压:对生产速度高于消费速度的消息流,用channel: Channel.new(n)让tell/ask在满时阻塞。

十、源码与测试导航

关注点位置
文档原文docs-source/erlang_actor.out.md、docs-source/erlang_actor.in.md
核心实现lib/concurrent-ruby-edge/concurrent/edge/erlang_actor.rb
mailbox 通道实现lib/concurrent-ruby-edge/concurrent/edge/channel.rb
模块加载入口lib/concurrent-ruby-edge/concurrent-edge.rb
行为/规则测试spec/concurrent/edge/erlang_actor_spec.rb
Edge 特性总览README.md

ErlangActor 作为concurrent-ruby-edge的实验性组件,接口仍可能演进;但其所承载的 Erlang 进程模型——消息传递、选择性接收、链接与监控、trap 与退出信号——是经多年生产验证的并发容错范式。用 Ruby 复刻这套语义,让 Ruby 开发者也能以 Erlang 的方式思考和构建并发系统,这正是 ErlangActor 的价值所在。若使用中发现与 Erlang 行为不一致之处,官方欢迎提交 issue 反馈。

  • 开发工具

【免费下载链接】concurrent-ruby

Modern concurrency tools including agents, futures, promises, thread pools, supervisors, and more. Inspired by Erlang, Clojure, Scala, Go, Java, JavaScript, and classic concurrency patterns.

项目地址:https://gitcode.com/gh_mirrors/co/concurrent-ruby
点击查看免费下载
上一篇:BlackHole 回环驱动手把手:macOS 系统音频录制一次搞懂
下一篇:ChatGPT-On-CS 部署教程:3分钟跑通的多平台智能客服

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询