Bash wait -n -p:先结束的是哪个任务,退出码又属于谁

10-01 3阅读

把三个检查任务放到后台后,父脚本需要知道谁先结束、哪个失败。只在最后执行不带参数的 wait,虽然能等待后台任务,却不能把每个任务的退出码逐一交出来。要建立可靠结果表,需要同时保留进程身份和等待状态。

wait -n 等待一个可收集的子任务,-p 将相应标识写进变量。父脚本再用启动时保存的进程编号查回业务名称。完成顺序可以变化,但结果属于哪个任务不能变化,这就是本例主要检验的关系。

不要按启动顺序猜完成顺序

将代码保存为 demo.sh,用 bash demo.sh 执行。本文在 Bash 5.2.37 验证,需要支持 wait -p 的 Bash。任务只做短暂本地等待并退出,不访问网络。它们分别返回零、七和一百二十七,父脚本负责完整收集。

Bash wait -n -p:先结束的是哪个任务,退出码又属于谁

AI概念示意图:表现本文的抽象关系,并非软件截图或真实运行结果。

set -u
declare -A names=() results=()
(sleep 0.04; exit 0) & names[$!]=alpha
(sleep 0.06; exit 7) & names[$!]=beta
(sleep 0.08; exit 127) & names[$!]=gamma
sleep "${DEMO_WAIT_DELAY:-0}"
while ((${#names[@]})); do
  if wait -n -p ended; then rc=0; else rc=$?; fi
  if [[ -v ended ]]; then
    results[${names[$ended]}]=$rc
    unset 'names[$ended]'
  elif ((rc == 127)); then
    for pid in "${!names[@]}"; do
      if wait "$pid"; then rc=0; else rc=$?; fi
      results[${names[$pid]}]=$rc
      unset 'names[$pid]'
    done
  else
    printf 'wait interrupted: %s\n' "$rc" >&2
    exit 1
  fi
done
[[ ${results[alpha]} == 0 ]] || exit 1
[[ ${results[beta]} == 7 ]] || exit 1
[[ ${results[gamma]} == 127 ]] || exit 1
for name in alpha beta gamma; do
  printf '%s=%s\n' "$name" "${results[$name]}"
done
printf 'wait checks passed\n'

非零状态也可能是一个正常收集结果

wait 返回非零并不意味着没有结果:beta 的退出码七就是我们要保存的数据。代码把 wait 放进条件分支,立即保存其状态,然后检查 ended 是否存在。若得到了任务标识,就把状态存到对应名称下,并从待收集表中删除该编号。

gamma 刻意返回一百二十七,说明不能只凭这个数字判断“已经没有任务”。当 ended 已设置时,它属于一个明确的子任务;只有没有标识且状态为一百二十七,才走剩余编号逐个等待的分支。

这个补充分支还覆盖父脚本开始收集太晚的情况。执行 DEMO_WAIT_DELAY=0.2 bash demo.sh,让短任务先结束,再进入循环,也应得到相同结果表。本文两种运行都通过,输出按名称固定为 alpha=0、beta=7、gamma=127。

把竞态留在完成顺序里,不带进结果归属

短暂停顿只是帮助观察,不是完成先后的保证。操作系统调度可能改变顺序,所以断言不要求某任务必须第一个返回。输出时再按名称整理,既保住实际归属,也使验证结果稳定。

-p 变量会在等待开始时被取消设置,因此启用未定义变量检查后,不能无条件读取它。示例先使用 -v 检查是否存在。若等待被信号中断且没有拿到任务标识,则报错退出,不把这次中断伪装成某个任务失败。

这里的独立脚本只启动这三个后台任务,因此不带编号的 wait -n 范围明确。若以后加入其他后台服务或进程替换,必须重新约束等待对象,并维护相应映射,不能把不认识的进程编号直接当作已有业务任务。

这份示例是结果收集器,不负责失败后取消其他任务,也不限制并发数量。实际批处理应另行决定一个任务失败后是否继续、如何保留日志,以及父进程中断时怎样结束仍在运行的工作。先把每个状态记对,后面的控制策略才有可靠依据。

启动时立即读取 $!,是为了在下一次后台启动覆盖它之前建立名称映射。不要先启动所有任务,最后只保存一次这个变量;那样只能知道最近启动的任务,前面两个结果就失去了可靠身份。进程编号只在本次父子进程关系内使用,不作为长期业务编号保存。

参考资料

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。