shell无原生线程,其“并发”实为多子进程并行;控制并发数需限制后台任务提交节奏,核心方法有三:wait分批阻塞、命名管道模拟信号量、jobs+wait-n动态调度。

Shell 本身没有线程概念,所谓“并发”本质是启动多个子进程并行运行。控制“同时运行的进程数”,关键在于**限制后台任务的并发提交节奏**,避免一次性 fork 出成百上千个进程拖垮系统。核心思路就两个:要么用 wait 配合分批提交,要么用 命名管道(FIFO)模拟信号量。下面说清楚怎么选、怎么写。
用 wait 分批阻塞最简单直接
适合任务总数不大(比如几十到一两百)、逻辑清晰、不想引入管道复杂度的场景。原理是:每提交 N 个后台任务,就用 wait 卡住,等这 N 个全结束再提交下一批。
- 设定并发上限,比如
MAX_PARALLEL=4 - 循环中每次提交一个任务:
your_command & - 用计数器判断是否达到上限:
((i % MAX_PARALLEL == 0)) && wait - 循环结束后补一个
wait,收尾所有剩余后台任务
示例片段:
for i in {1..20}; do{ echo "task $i"; sleep 2; } &
(( i % 4 == 0 )) && wait
done
wait
用命名管道实现精确并发控制
适合任务量大、对资源占用敏感(如 CPU/内存受限)、需要稳定维持固定并发数的生产脚本。它靠 FIFO 管道充当“令牌池”:每个任务开始前先从管道读一行(拿令牌),执行完再写回一行(还令牌)。管道初始预填 N 行,就天然限流为 N 个并发。
Shell本身是一个用C语言编写的程序,它是用户使用Linux的桥梁。Shell既是一种命令语言,又是一种程序设计语言。作为命令语言,它交互式地解释和执行用户输入的命令;作为程序设计语言,它定义了各种变量和参数,并提供了许多在高级语言中才具有的控制结构,包括循环和分支。它虽然不是Linux系统核心的一部分,但它调用了系统核心的大部分功能来执行程序、建立文件并以并行的方式协调各个程序的运行。因此,对于用户来说,shell是最重要的实用程序,深入了解和熟练掌握shell的特性极其使用方法,是用好Linux系统
- 创建临时 FIFO:
mkfifo /tmp/$$.fifo - 用
exec 6xxx.fifo绑定文件描述符 - 预写入 N 个换行符:
for((i=0;i<n echo>&6; done</n> - 主循环里:
read -u6阻塞等待可用令牌,再启动任务并echo >&6归还
这种方式不会因任务执行快慢而波动,并发数始终严格等于初始写入行数。
用 jobs + wait -n 动态调度更灵活
不依赖外部文件或管道,纯靠 Shell 内置命令监控后台作业状态。适合想实时感知完成情况、或需配合错误处理的场景。
- 维护一个计数器
running=0记录当前活跃进程数 - 每次提交前检查:
if [ $running -lt $MAX ]; then ... &; ((running++)); fi - 用
wait -n等待任意一个后台任务退出,然后((running--)) - 循环直到所有任务入队且全部结束
优势是逻辑透明、无临时文件残留;缺点是 shell 脚本里频繁调用 jobs -r | wc -l 效率略低,但对几百以内任务完全够用。
别踩这些坑
漏写 wait:没它,后续命令会立刻执行,根本等不到并发任务结束。
并发数设太高:比如在 2 核机器上开 100 个 sleep 进程,CPU 上下文切换开销反超收益。
管道不清理:rm $tmpfile 必须在 exec 关闭之后,否则可能删不掉或影响下次运行。
子 shell 变量失效:{ ... }& 会创建子 shell,里面改的变量父脚本看不到,结果汇总要靠日志或临时文件。










