0

bash FIFO令牌桶实现并发控制完整教程:3种写法让你的批量任务跑满不超载

2026.08.10 | youres | 230次围观

写批量脚本的时候,你有没有遇到过这种两难:并行数设低了效率上不去,设高了服务器又扛不住。用 bash 的 FIFO 加 wait -n 搭一个令牌桶,就能把这事解决得很漂亮。

什么是令牌桶?为什么它适合做并发控制

令牌桶的核心逻辑很简单:桶里有固定数量的"令牌",每个任务启动前必须从桶里取走一个令牌,任务结束后把令牌还回去。这样同时运行的任务数量永远不会超过桶的容量。换成 bash 的说法就是:创建 N 个 FIFO,启动任务前从某个 FIFO 读一把,任务结束时往另一个 FIFO 写一把。整个过程不需要任何第三方工具,bash 原生就能跑。

方法一:双 FIFO 方案(最常用)

一端放令牌,另一端收令牌,逻辑最清晰。

#!/bin/bash MAX_JOBS=4 TOKEN_FIFO=/tmp/token_bucket_$$ BACK_FIFO=/tmp/back_bucket_$$ # 创建两个 FIFO mkfifo $TOKEN_FIFO $BACK_FIFO # 先往令牌桶里放满令牌 for ((i=0;i<MAX_JOBS;i++)); do echo > $TOKEN_FIFO done # 后台任务:把完成的令牌还回去 reclaimer() { while true; do read < $BACK_FIFO echo > $TOKEN_FIFO done } reclaimer & # 任务函数 do_job() { echo "任务开始: $1" sleep 2 echo "任务完成: $1" echo > $BACK_FIFO # 归还令牌 } # 启动任务 for i in {1..10}; do read < $TOKEN_FIFO # 取令牌(阻塞直到有令牌可用) do_job "job_$i" & done wait rm -f $TOKEN_FIFO $BACK_FIFO

这层的关键在于:read < $TOKEN_FIFO 会一直阻塞,直到桶里有空闲令牌。这样即使一口气提交 10 个任务,实际同时跑的也只有 4 个。

方法二:单 FIFO 加超时控制(更简洁)

令牌直接记录活跃槽位,一个 FIFO 搞定一切。

#!/bin/bash MAX_JOBS=4 QUEUE_FIFO=/tmp/job_queue_$$ mkfifo $QUEUE_FIFO # 往队列里放 N 个槽位 for ((i=0;i<MAX_JOBS;i++)); do echo "slot" > $QUEUE_FIFO done # 任务函数 do_job() { echo "处理中: $1" sleep 1 echo "完成: $1" } # 提交任务 for i in {1..8}; do read -t 1 < $QUEUE_FIFO # 取槽位 ( do_job "item_$i" echo "slot" > $QUEUE_FIFO # 完成后还槽位 ) & done wait rm -f $QUEUE_FIFO

注意这里用了 read -t 1 加 1 秒超时,目的是让主循环不会在所有槽位都空着时永久阻塞。实际使用中根据任务预估耗时调整超时时间即可。

方法三:用 exec 9 打开读写双端(高级写法)

把 FIFO 同时以读写方式打开到文件描述符 9,让它成为一个永不阻塞的令牌管道。

#!/bin/bash MAX_JOBS=3 FIFO=/tmp/token_$$ mkfifo $FIFO exec 9<>$FIFO # 往读写端放进令牌 for ((i=0;i<MAX_JOBS;i++)); do echo "-" >&9 done do_job() { echo "开始: $1" sleep 2 echo "结束: $1" } for i in {1..6}; do read line <&9 # 消耗一个令牌 ( do_job "task_$i" echo "-" >&9 # 归还令牌 ) & done wait exec 9<&- rm -f $FIFO

exec 9<>$FIFO 打开读写端后,FIFO 本身就有读端和写端,所以永远不会报"FIFO 没有读者"而阻塞。适合需要更精确控制的复杂场景。

三种方案横向对比

实际选哪个,取决于你的场景。

  • 双 FIFO 方案:逻辑最清晰,调试最容易,适合大多数批量任务场景。
  • 单 FIFO 加超时:代码最简洁,适合任务耗时稳定、可以预估超时时间的场景。
  • exec 读写双端:控制最精确,适合对性能要求高、需要在任务内部精确管理令牌归还的场景。

并发数怎么定

令牌桶的容量不是拍脑袋定的,有个参考公式:并发数 ≈ CPU 核心数 × 2(适用于 I/O 密集型任务)。如果是纯 CPU 计算,则保持在核心数左右。设完之后观察系统负载和任务总耗时,找到最优值。

还有个细节:网络请求类任务别只看系统负载,还要关注对方服务的限流阈值。令牌桶能控制本地并发,但发请求之前最好查清楚目标 API 的 QPS 上限。

写在最后

FIFO 令牌桶的好处是整个机制一目了然:桶里有多少令牌,同时就在跑多少任务。不需要装任何依赖,纯 bash 原生实现。如果你现在用的是 sleep 循环来控并发,或者干脆不控并发导致服务器爆掉,换成这个方案会稳很多。

实际项目中,令牌桶往往会配合 trap 一起用,确保脚本被中断时 FIFO 能被正确清理,不会留下垃圾文件。

相关推荐

版权声明

本文仅代表个人观点。
本文系AI辅助作者原创,未经许可,转载请保留原文链接。

发表评论