如何从Bash脚本中并行运行多个程序?

485

我想编写一个.sh文件,可以同时运行多个程序。

我尝试了以下代码:

prog1 
prog2

但是这样会运行prog1,等待直到程序结束,然后再开始运行prog2...

那么如何实现并行运行它们?

19个回答

489

如何尝试:

prog1 & prog2 && fg

这个命令会执行以下操作:

  1. 启动 prog1
  2. 将其放入后台,但仍然打印输出。
  3. 启动 prog2 并将其保持在前台,这样您可以使用 ctrl-c 来关闭它。
  4. 当您关闭 prog2 后,您将返回到 prog1 的前台,因此您也可以使用 ctrl-c 关闭它。

14
prog2终止时,是否有一种简单的方法来终止prog1?请考虑node srv.js&cucumberjs - jpsecher
34
我试过这个方法,但结果并不如预期。不过,稍加修改后可以实现:prog1&prog2; fg这是为了同时运行多个ssh隧道。希望能对某些人有所帮助。 - jnadro52
2
@jnadro52,你的解决方案会导致如果prog2无法立即运行,你将回到在前台运行prog1的状态。如果这是可取的,那就没问题。 - Ory Band
3
在SSH连接的终端中,如果你执行如下命令,想要结束prog1会很棘手。即使使用Ctrl+c也没用,就算关闭整个终端,prog1依然会继续运行。 - mercury0114
29
@jnadro52 一种同时终止两个进程的方法是 prog1&prog2 && kill $! - zaboco
显示剩余5条评论

457

同时运行多个程序:

prog1 &
prog2 &

如果您需要脚本等待程序完成,可以添加:

wait

在您希望脚本等待它们的位置。


95
别忘了wait命令!在Bash中,你可以等待脚本的子进程。 - Dummy00001
7
另一种选择是使用 nohup 命令,防止程序在 shell 断开连接时被终止。 - Philipp
1
@liang:是的,它也可以与三个或更多程序一起使用。 - psmears
1
也许这是一个愚蠢的问题,但如果我想运行 prog1 | something & prog2 | another & 会怎么样呢?我很确定它不会起作用。 - Micha93
1
@Micha93:它运行得很好,你为什么认为它不会呢? - psmears
显示剩余8条评论

262

如果你想要轻松地运行和终止多个进程,使用ctrl-c,这是我最喜欢的方法:在一个(…)子shell中生成多个后台进程,并捕获SIGINT来执行kill 0,这将会终止所有在子shell组中生成的进程:

(trap 'kill 0' SIGINT; prog1 & prog2 & prog3)

你可以拥有复杂的进程执行结构,并且一切都将在单个 ctrl-c 下关闭(只需确保最后一个进程在前台运行,即不要在 prog1.3 后面加上 &):

(trap 'kill 0' SIGINT; prog1.1 && prog1.2 & (prog2.1 | prog2.2 || prog2.3) & prog1.3)

如果存在最后一条命令可能会提前退出并且您想让其他所有内容继续运行,请将wait添加为最后一条命令。在以下示例中,sleep 2 将首先退出,杀死 sleep 4,然后才能完成;添加wait允许两者都运行到完成:

(trap 'kill 0' SIGINT; sleep 4 & sleep 2 & wait)

25
到目前为止,这是最佳答案。 - Nic
4
“kill 0”是什么意思?是指PID为0的子shell本身吗? - mpen
6
@mpen,没错,kill程序将0解释为“当前进程组中的所有进程都会被信号终止。”这个描述在man手册中有提到。 - Quinn Comendant
5
(trap 'kill 0' SIGINT; prog1 & prog2 & prog3 & wait)有助于确保所有程序都能顺利完成。 - jakeonfire
2
这是一个很好的答案,但我建议您先移动更新的“wait”版本,并将其作为主要解决方案。99%的情况下,这就是用户所期望的。 - Steven Spungin
显示剩余8条评论

138

您可以使用wait

some_command &
P1=$!
other_command &
P2=$!
wait $P1 $P2

它将后台程序的PID分配给变量($! 是最后启动的进程的PID),然后wait命令等待它们。如果你杀死脚本,它也会杀死这些进程,这很好!


13
根据我的经验,杀死等待进程并不会影响其他进程。 - Quinn Comendant
1
如果我在循环中启动后台进程,如何等待每个后台进程完成后再继续执行下一组命令。`#!/usr/bin/env bashARRAY='猫 蝙蝠 鼠'for ARR in $ARRAY do ./run_script1 $ARR & doneP1=$! wait $P1echo "INFO: for循环中所有后台进程的执行已完成.."` - Yash
2
使用wait无法结束我的第二个进程。 - frodo2975
1
非常棒的答案。如果出现故障,是否还可以捕获退出代码以中止操作? - openCivilisation
我有与上面评论相同的问题,请等待在Ctrl+C命令后不要杀死第二个进程。 - Ângelo Polotto
显示剩余3条评论

97

11
值得注意的是,parallel 存在不同语法的不同版本。例如,在 Debian 派生系统中,moreutils 软件包包含一个名为 parallel 的不同命令,其行为有很大不同。 - Joel Cross
9
“parallel”比使用“&”符号更好吗? - Optimus Prime
6
@OptimusPrime 这要看情况。GNU Parallel 会增加一些额外负担,但是相应地,它给予你更多关于任务运行和输出的控制权。如果两个任务同时输出,GNU Parallel 将确保它们不会混合在一起。 - Ole Tange
2
@OptimusPrime 当有更多的任务而不是核心时,parallel 更好,此时 & 会同时在一个核心上运行多个任务。 (参见 鸽巢原理) - Geremia
1
这是改变生活的重要时刻。 - kargirwar

34

xargs -P <n> 允许您并行运行 <n> 个命令。

虽然 -P 是一个非标准选项,但 GNU (Linux) 和 macOS/BSD 实现都支持它。

下面的例子:

  • 最多同时运行 3 个命令,
  • 只有在先前启动的进程终止时,才会启动其他命令。
time xargs -P 3 -I {} sh -c 'eval "$1"' - {} <<'EOF'
sleep 1; echo 1
sleep 2; echo 2
sleep 3; echo 3
echo 4
EOF

输出看起来像这样:

1   # output from 1st command 
4   # output from *last* command, which started as soon as the count dropped below 3
2   # output from 2nd command
3   # output from 3rd command

real    0m3.012s
user    0m0.011s
sys 0m0.008s

计时显示这些命令是并行运行的(最后一个命令仅在原始3个命令中的第一个终止之后启动,但执行非常快)。 xargs 命令本身不会返回,直到所有命令都完成,但可以通过使用控制操作符 & 终止它并在后台执行,然后使用 wait 内置命令等待整个 xargs 命令完成。
{
  xargs -P 3 -I {} sh -c 'eval "$1"' - {} <<'EOF'
sleep 1; echo 1
sleep 2; echo 2
sleep 3; echo 3
echo 4
EOF
} &

# Script execution continues here while `xargs` is running 
# in the background.
echo "Waiting for commands to finish..."

# Wait for `xargs` to finish, via special variable $!, which contains
# the PID of the most recently started background process.
wait $!

注意:

  • 在BSD/macOS中,xargs要求你明确指定要并行运行的命令数量,而GNU xargs允许你使用-P 0来尽可能地并行运行。

  • 并行运行的进程输出会随着生成而到达,因此它们会以不可预测的方式交错。

    • 正如Ole's answer所提到的,GNU parallel(大多数平台不自带)方便地按照每个进程的基础对输出进行序列化(分组),并提供了许多高级功能。

16

这是一个我用来最多并行运行n个进程的函数(在此例子中n等于4):

max_children=4

function parallel {
  local time1=$(date +"%H:%M:%S")
  local time2=""

  # for the sake of the example, I'm using $2 as a description, you may be interested in other description
  echo "starting $2 ($time1)..."
  "$@" && time2=$(date +"%H:%M:%S") && echo "finishing $2 ($time1 -- $time2)..." &

  local my_pid=$$
  local children=$(ps -eo ppid | grep -w $my_pid | wc -w)
  children=$((children-1))
  if [[ $children -ge $max_children ]]; then
    wait -n
  fi
}

parallel sleep 5
parallel sleep 6
parallel sleep 7
parallel sleep 8
parallel sleep 9
wait

如果 max_children 设为核心数量,此函数将尝试避免空闲的核心。


1
不错的代码片段,但我找不到“wait -n”的解释,在我的bash下它说这是一个无效的选项。是打错了还是我漏掉了什么? - Emmanuel Devaux
3
"wait -n" 命令需要 bash 4.3+ 版本,并且它会改变等待进程结束的逻辑,使其等待任何指定/暗示的进程中的一个结束。 - mklement0
如果其中一个任务失败了,那么我想要结束脚本吗? - 52coder
@52coder 你可以调整函数来捕获失败的子进程,类似这样:"$@" && time2=$(date +"%H:%M:%S") && echo "完成 $2 ($time1 -- $time2)..." || error=1 &.然后在 "if" 部分测试错误,并在需要时中止函数。 - arnaldocan
感谢wait -n命令。我认为它会对这个相关问题的好答案有所帮助。 - teichert

15
#!/bin/bash
prog1 & 2> .errorprog1.log; prog2 & 2> .errorprog2.log

将错误重定向到单独的日志中。


13
你需要将"&"符号放在重定向符号之后,同时省略分号("&"符号也会充当命令分隔符的功能):prog1 2> .errorprog1.log & prog2 2> .errorprog2.log & - Dennis Williamson
分号可以执行两个命令,你可以测试bash以确保它正常工作 ;)例如: pwd & 2> .errorprog1.log; echo "wop" & 2> .errorprog2.log当你使用&时,你将程序放在后台并立即执行下一个命令。 - fermin
2
它不起作用 - 错误没有被重定向到文件。尝试使用:ls notthere1 & 2> .errorprog1.log; ls notthere2 & 2>.errorprog2.log。错误信息会输出到控制台,两个错误文件都是空的。正如@Dennis Williamson所说,&是一个分隔符,就像;一样,所以(a)它需要放在命令的末尾(在任何重定向之后),(b)你根本不需要; :-) - psmears

9
这对我非常有效(在这里发现):
sh -c 'command1 & command2 & command3 & wait'

它将每个命令的所有日志混合输出(这正是我想要的),并且所有命令都可以通过按下ctrl+c来结束。

但是当你使用 ctrl+c 时,子进程不会在后台继续运行吗? - Leonardo Raele

8

有一个非常有用的程序叫做 nohup。

nohup - run a command immune to hangups, with output to a non-tty

5
nohup 命令本身并不会将任何东西放在后台运行,使用 nohup 也不是在后台运行任务的必要条件或前提。虽然它们经常一起使用,但这并不能回答问题。 - tripleee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接