三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

并发入门实战:通过 Exercism Elixir 的 Take-a-Number 系列理解进程、PID 与 GenServer

并发入门实战:通过 Exercism Elixir 的 Take-a-Number 系列理解进程、PID 与 GenServer

并发入门实战:通过 Exercism Elixir 的 Take-a-Number 系列理解进程、PID 与 GenServer

【免费下载链接】elixirExercism exercises in Elixir.项目地址: https://gitcode.com/gh_mirrors/elixi/elixir

摘要

想在实战中理解 Elixir 并发编程的核心概念?Exercism 的 Take-a-Number 系列练习正是为此而生。本文将通过「排队叫号机」这一贴近生活的场景,带你一步步掌握Elixir 进程的创建、PID的本质、进程间消息传递,以及如何用 OTP 的GenServer优雅地管理并发状态。无论你是 Elixir 新手,还是想补全并发知识体系的学习者,这份实战指南都能帮你建立从裸进程到 GenServer 的完整认知链路。


为什么用「排队叫号机」来学习 Elixir 并发?

银行、医院里的叫号机,本质上是一个有状态的服务进程:它记住当前发到几号,每次叫号递增,还能支持重置。这个模型与 Elixir 的进程模型几乎一一对应:

  • 叫号机 = 一个 Elixir 进程
  • 顾客手里的号码 = 进程之间的消息
  • 当前号码 = 进程内部维护的状态

Exercism Elixir 官方仓库中,exercises/concept/take-a-number/exercises/concept/take-a-number-deluxe/两个练习构成了完美的进阶路线:前者让你手写全部并发原语,后者让你用OTP 的 GenServer重写同样的逻辑。同样的业务,两种实现,对比学习的效果远胜死记硬背。

对比维度take-a-numbertake-a-number-deluxe
进程创建spawn/1手动创建GenServer.start_link/3
消息处理手写receive循环handle_call/handle_cast/handle_info
状态管理递归参数传递GenServer 内置状态
难点等级⭐ 入门⭐⭐ 进阶

第一步:用 spawn 创建 Elixir 进程并理解 PID

exercises/concept/take-a-number/lib/take_a_number.ex中,你的第一个任务(测试文件take_a_number_test.exs的 task 1)是实现start/0,返回一个新的进程标识符(PID)

Elixir 中所有代码都运行在进程里,spawn/1可以显式地在全新进程中执行函数:

pid = spawn(fn -> :ok end) # => #PID<0.125.0>

这里的关键认知有三点:

  1. PID 就是进程的邮箱地址——只要拿到 PID,就能向该进程投递消息(对应概念文档concepts/pids/introduction.md);
  2. Elixir 进程极其轻量——它不是操作系统进程,内存和 CPU 开销都极小,一个应用跑成百上千个进程毫无压力;
  3. 每个进程都有独立的邮箱——消息先进入邮箱排队,只有进程主动receive才会被取出(见concepts/processes/introduction.md)。

测试会校验is_pid(pid)pid != self(),确保你返回的是其他新进程的 PID,而不是当前进程。

第二步:Elixir 进程间通信的核心——send 与 receive 消息传递

掌握了 PID 之后,第二步就是用send/2receive/1让进程真正"说话"。take-a-number 的 task 2 要求实现:收到{:report_state, sender_pid}消息后,把当前号码发回给发送者。

send(pid, {:report_state, self()}) # 发消息:报告你的状态 receive do number -> number # 收到回复 end

而进程内部则用receive/1模式匹配邮箱里的消息:

receive do {:report_state, sender_pid} -> send(sender_pid, current_number) :stop -> exit(:normal) end

理解这条消息传递链路,就理解了 Elixir 并发的全部秘密——进程之间不共享内存,只交换消息。想收到回复,就必须把发送者的 PID(self())放进消息里,这是 Elixir 消息传递最经典的习惯用法。

第三步:用 receive 循环维护进程状态

光能收发一次消息还不够,叫号机必须持续服务。task 3 要求连续发出 1、2、3、4、5……这需要进程在receive之后递归调用自己,把新的号码作为参数传入下一轮——这就是经典的receive loop模式:

def loop(current_number) do receive do {:report_state, sender_pid} -> send(sender_pid, current_number) loop(current_number) # 状态不变,继续循环 {:take_a_number, sender_pid} -> send(sender_pid, current_number + 1) loop(current_number + 1) # 状态递增,继续循环 :stop -> :ok # 退出循环,进程结束 end end

状态不放在变量里"原地修改",而是作为递归参数不断传递——这是函数式并发的精髓。task 4 和 task 5 还会要求你处理:stop消息优雅退出,并忽略未知消息(比如有人对叫号机喊"我要找经理" 😄),保证进程不被意外消息搞崩溃。

第四步:从裸进程升级到 GenServer——OTP 行为实战

手写 receive loop 能帮你理解原理,但生产级 Elixir 项目几乎都用GenServer(generic server,通用服务器)来封装这套模式。它本质上就是把你手写的 receive loop 抽象成了可复用的行为(behaviour),概念细节见concepts/genserver/introduction.md

exercises/concept/take-a-number-deluxe/lib/take_a_number_deluxe.ex中,练习给出了完整的客户端 API 骨架

  • start_link/1—— 启动进程(对应init/1回调)
  • report_state/1—— 查询状态(同步,handle_call
  • queue_new_number/1—— 取新号(同步,handle_call
  • serve_next_queued_number/2—— 叫号,支持优先号(同步,handle_call
  • reset_state/1—— 重置叫号机(异步,handle_cast

你需要实现的正是三个核心回调:

回调对应客户端函数语义典型返回值
init/1start_link/1初始化状态{:ok, state}
handle_call/3call/2系函数同步请求,需要回复{:reply, reply, state}
handle_cast/2cast/2系函数异步请求,无需回复{:noreply, state}
handle_info/2直接send/2处理任意消息{:noreply, state}

状态则用结构体封装在exercises/concept/take-a-number-deluxe/lib/take_a_number_deluxe/state.ex中:min_numbermax_numberqueue(号码队列,实现见同目录下的queue.ex)、auto_shutdown_timeout(自动关闭超时)。

deluxe 版还藏着一个进阶彩蛋——超时自动关闭init/1返回{:ok, state, timeout}后,若指定毫秒数内没有消息到达,进程会自动触发handle_info(:timeout, state)实现自我回收。这是 GenServer 回调返回三元组的一个绝佳实战案例。

实践指南:3 步跑通 Take-a-Number 系列练习

  1. 获取练习源码git clone https://gitcode.com/gh_mirrors/elixi/elixir,进入exercises/concept/take-a-number/目录;
  2. 阅读测试驱动开发:先看test/take_a_number_test.exs,每个测试都标注了task_id,按 1→5 的顺序逐个实现lib/take_a_number.ex中的函数;
  3. 运行测试验证:在练习目录执行mix test,绿灯全亮后,再挑战 deluxe 版本(take-a-number-deluxe/),体会 GenServer 如何把同样逻辑写得更加清晰。

总结:一条通往 Elixir 并发的完整学习路径

通过 Take-a-Number 系列,你实际上完成了一次从底层到框架的完整进阶:

  • ✅ 用spawn创建Elixir 进程,理解PID作为邮箱地址的本质
  • ✅ 用send/receive完成进程间消息传递
  • ✅ 用receive loop 递归维护进程状态
  • ✅ 用GenServer封装状态与消息处理,掌握 OTP 行为三件套handle_call/handle_cast/handle_info

当你能用两种方式实现同一台"叫号机",Elixir 的并发模型就已经不再是抽象概念,而是你信手拈来的实战技能了。下一步,不妨把 take-a-number-deluxe 的队列换成TaskAgent再实现一遍——并发世界的大门,正为你敞开。🚀

【免费下载链接】elixirExercism exercises in Elixir.项目地址: https://gitcode.com/gh_mirrors/elixi/elixir

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

← 返回列表