shell通过后台进程(&)+ wait + pid管理实现逻辑并发:用&启动任务并捕获$!,用wait等待全部完成,封装函数支持超时与结果收集,注意文件竞争、信号处理、错误码丢失和环境隔离。

Shell 本身不支持线程或真正的异步 I/O,但可以通过**后台进程(&)+ wait + 进程管理**实现逻辑上的“异步并发”。关键不是模拟异步,而是让多个独立任务并行启动、各自运行、统一收尾——这正是 Shell 多任务并发的核心思路。
用 & 启动后台任务,避免串行阻塞
在函数内部调用耗时命令(如 ping、curl、ssh)时,末尾加 & 将其转入后台。这样函数不会等待它结束,立刻进入下一次迭代或返回。
- 每个后台任务会获得一个唯一的进程 ID(PID),可通过
$!捕获 - 注意:变量作用域在子 shell 中受限,需避免在
&后直接修改父脚本变量 - 示例:启动 3 个并发 ping 并记录 PID
# 在函数中
for ip in 192.168.0.{1..3}; do
ping -c 1 "$ip" >/dev/null && echo "$ip up" || echo "$ip down" &
pids+=($!)
done
用 wait 等待全部完成,确保结果完整
仅靠 & 启动不够——主流程可能提前退出,导致后台任务被中断或输出混乱。必须显式调用 wait 等待所有子进程结束。
-
wait不带参数:等待当前 shell 启动的所有后台作业 -
wait $pid:等待指定 PID 的进程,适合精细化控制 - 建议配合数组存储 PID,并循环
wait ${pids[@]},避免遗漏
# 紧接上段代码
wait "${pids[@]}"
echo "All pings completed"
封装成可复用函数:带超时与结果收集
把并发逻辑封装为函数,支持传入命令模板、参数列表和超时时间,提升复用性:
- 使用
printf '%s\0' "${args[@]}" | xargs -0 -n1 -P $max_jobs是另一种方案,但灵活性低、不易捕获各任务状态 - 推荐手动管理:循环构建命令 →
eval或bash -c执行 → 记录 PID → 收集输出到临时文件或数组 - 为防死锁,对每个后台任务加超时(如
timeout 5s ping -c 1 $ip)
# 示例函数骨架
concurrent_run() {
local max_jobs=${1:-4} cmd args=() pids=() results=()
shift; cmd="$1"; shift; args=("$@")
for arg in "${args[@]}"; do
timeout 3s bash -c "$cmd" _ "$arg" >"out.$$.${arg}" 2>&1 &
pids+=($!)
done
wait "${pids[@]}"
for arg in "${args[@]}"; do
results+=("$(cat "out.$$.${arg}")")
rm -f "out.$$.${arg}"
done
printf '%s\n' "${results[@]}"
}
# 调用:concurrent_run 3 'echo "result for $1"' host1 host2 host3
注意事项与避坑点
实际使用中容易忽略几个关键细节:
-
资源竞争:多个后台任务同时写同一文件需加锁(可用
flock),或改用独立临时文件 -
信号处理:主脚本收到
SIGINT(Ctrl+C)时,默认不转发给后台进程;可用trap 'kill $(jobs -p) 2>/dev/null' INT清理 -
错误码丢失:后台命令的
$?在父 shell 中不可见;必须通过文件、命名管道或wait -n(Bash 4.3+)逐个检查退出状态 - 环境隔离:后台进程继承父 shell 环境,但无法反向影响父 shell 变量;如需汇总数据,优先走文件或标准输出重定向











