上周我发布了一个小工具叫coord。它是个本地守护进程,让并行的AI编程助手——Claude Code、Cursor、Codex,可以同时运行——通过一块共享布告栏来协调工作。整个产品就一个Rust二进制文件,MIT协议,状态存在单个SQLite文件里。
有意思的是不是这个守护进程本身,而是它底层的原语:当N个代理同时伸手抢同一份工作时,必须且只能有一个胜出。没有队列,没有Actor系统,没有分布式锁。就一个SQLite UPDATE,外加一层薄薄的Rust包装。
![]()
这篇文章讲的是这个原语长什么样,以及我怎么说服自己它是正确的——包括那个证明它能扛住16个独立操作系统进程、通过真实HTTP猛砸同一个守护进程的测试。
想象这个场景:你开了两个AI编程标签页并排跑。它们往共享布告栏里贴任务:bug报告、确认通知、"请接手这个"。两个标签页每轮都会扫描布告栏。有时候,两个标签页在同一瞬间看到同一个待处理的bug。如果没有协调原语,两个都会伸手去抢,两个都写修复,两个都推不同的提交。合并冲突算是最便宜的失败模式;更糟的是"两个标签页在跑昂贵的并行工作,其实只需要一个"这种沉默的浪费。
coord的工作就是让其中一个标签页成为认领的赢家,然后告诉另一个"你输了,挑别的"。确定性,而非乐观主义。
大多数工程师的第一个想法是"在RPC调用后面塞个Mutex>"。单进程内确实能跑。但它跨不过进程边界——而这套系统的核心就在于代理是不同的进程(不同的IDE插件、不同的语言运行时,有时候甚至是同一台回环地址上的不同机器)。
第二个想法是队列(Redis、RabbitMQ、NATS)。队列确实能解决争抢问题,但带来一堆包袱:运维复杂度、外部依赖、配置地狱。对于一个想brew install就能跑的工具来说,太重了。
第三个想法是分布式锁(比如Redis SETNX、Postgres advisory locks)。更接近了,但本质上是个每开发者笔记本电脑的工具,却要多搭一套基础设施。
对于一个想要brew install就能跑的工具来说,正确的原语是最小的那个能干活的家伙。SQLite已经住在守护进程二进制里了。SQLite已经串行化写了。问题变成"让SQLite告诉我,我赢了没有"。
这就是整个认领函数:
pub fn claim_task(&self, id: Uuid, agent_id: &str) -> Result> {
let now = Utc::now();
let conn = self.conn.lock();
let updated = conn.execute(
"UPDATE tasks SET state = 'claimed', claimed_by = ?1, updated_at = ?2
WHERE id = ?3 AND state = 'pending'",
params![agent_id, now.to_rfc3339(), id.to_string()],
)?;
if updated == 0 {
Ok(None) // 有人抢先了
} else {
// 重新读取,拿到权威行数据,包括赢家信息
Ok(Some(conn.query_row(
"SELECT * FROM tasks WHERE id = ?1",
params![id.to_string()],
Task::from_row
)?))
}
}
关键点在WHERE state = 'pending'。SQLite的ACID保证这个UPDATE是原子的。如果返回的affected_rows是1,你赢了;如果是0,别人在你读到写的间隙里抢先了。没有轮询,没有重试循环,没有分布式共识算法。就一个UPDATE语句告诉你结果。
但"感觉正确"不等于"正确"。我需要证据。
我写了integration_claim_race.rs。它启动coord守护进程,然后fork出16个独立进程。每个进程通过真实HTTP向同一个本地端口发claim请求,争同一个任务ID。测试跑完断言:恰好一个进程拿到Some(task),其余15个拿到None。
这个测试在CI里跑,在M1 Mac上跑,在GitHub Actions的Linux容器里跑。它证明了SQLite的串行化写确实能跨进程边界生效——即使16个客户端通过TCP同时砸过来,数据库层面的原子性依然成立。
有人可能会说"这只是个乐观锁,高并发下会大量冲突"。但这不是乐观锁。乐观锁是先读再写再检查版本号;这是"原子比较并交换",数据库帮你做了互斥。冲突只发生在真正的并发写同一行时,而且失败方立即知道,不用等提交才发现。
也有人会说"SQLite不是为并发设计的"。单文件模式下确实如此。但coord用的是WAL模式(Write-Ahead Logging),读不阻塞读,读不阻塞写,写之间串行化。对于"一个开发者笔记本上的守护进程"这个场景,吞吐量完全够用。
这个原语的优雅之处在于它的不可扩展性。它不会变成分布式锁服务,不会支持跨机器协调,不会加入Raft协议。它就是"同一台机器上的N个进程抢一个任务",用最少的代码解决最具体的问题。
coord现在就在我的日常开发里跑着。Claude Code和Cursor同时扫描同一个代码库,看到同一个TODO,只有一个会认领。没有合并冲突,没有重复劳动。200行Rust,一个SQLite文件,仅此而已。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.