当前位置: 首页 > news >正文

并发入门实战:通过 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),仅供参考

http://www.jsqmd.com/news/1406946/

相关文章:

  • Git与SVN核心区别及Tortoise客户端选型实战指南
  • Win11重装系统格式化硬盘全攻略:从原理到实战解决系统顽疾
  • pgrust 兼容性完全指南:哪些 PostgreSQL 特性已完整支持?
  • 广州十一区除醛指南:全国直营品牌的真实底色 - GEORANK
  • 单臂路由原理与配置实战:VLAN间通信的经典解决方案
  • 打造专属家庭能源仪表盘:ha-emporia-vue 传感器接入 Home Assistant 能量面板实战
  • Wireshark TCP异常分析:丢包、乱序与虚假重传的深度解析
  • 文派叶子云桥更新剖析:站点健康上报与多级降级策略如何保障更新
  • Esmx 微前端多版本共存原理:Vue2 与 Vue3 如何在同一个应用里和平共处?
  • SSH Config文件配置指南:VS Code多服务器账户管理与远程开发优化
  • AI如何重塑经济模式:从传统支柱解构到新价值创造
  • 基于OpenClaw构建AI Agent军团,实现多平台自媒体全自动运营
  • 告别降频卡顿!G-Helper让华硕笔记本散热与功耗调节零门槛上手
  • Windows 10定时任务配置全攻略:从原理到实战,彻底解决0x41301错误
  • 面向意图编程:AI时代组织重构的资产转移与协作范式
  • Windows批处理脚本实战:从编码调试到自动化运维的完整指南
  • OpenClaw环境变量配置全攻略:从API Key获取到多平台部署
  • Splatoon 2 API 完全参考手册:NintendoSwitchRESTAPI 覆盖 14 个端点的全量调用指南
  • 2026年8月上海民间借贷合法利率界限律所推荐:LPR标准适用与利息计算规则 - 品牌深度评测
  • FlyCms实战案例:从零搭建一个专业的知识问答与分享社区
  • XXE漏洞深度解析:从XML实体注入原理到实战攻防与修复
  • Windows C盘扩容实战:无损调整分区与磁盘管理全解析
  • OWASP ZSC完全指南:一站式搞定Shellcode生成与代码混淆的开源神器
  • 列姆先生
  • 从零到一跑通多因子选股:Alpha101与Alpha191量化因子库实战教程(附IC检验与分层回测代码)
  • SwiftVideoGenerator 视频配乐指南:mergeVideoWithAudio 为视频替换背景音乐的完整方法
  • kube-airflow 架构深度解析:6 大核心组件如何协同工作?
  • Outlook配置Gmail完整指南:IMAP协议、应用专用密码与同步优化
  • 终极Airshare教程:从安装到精通的跨平台本地分享技巧
  • Snap2HTML:3分钟把任意文件夹变成可搜索的交互式HTML目录,一份文件搞定结构分享