Newshot
Account

并发 Shell 命令队列

Translation(model translation, not the source's own text)

并发 Shell 命令队列

并发 Shell 命令队列
并发 Shell 命令队列

如何并行运行 Shell 命令并限制其并发数。

我最近有一个用例,我想加速一些我正在做的 Bash 处理,但仍然保持并发受限。所以我想知道:在 Bash 中创建线程池有没有一种直接的方法?答案是:有,但需要一些工作才能做到半可靠和可移植。

你可能会问:为什么要在 Bash 中做这件事?使用一个并发控制很差的语音似乎很傻。你问这个问题是对的,我的回答是:因为我对 Bash 有感情,而且我想要挑战。但你可能仍然最好使用另一种语言,比如 Python。

下面的代码创建了一个命令队列,可以接受要执行的 shell 命令。你不需要预先知道所有想要执行的命令,你可以随时间向它提交命令。


        1# Start this shell's command queue.
2# @param {number} $1 Maximum number of jobs to run at once
3queue_start() {
4  # Validate that the parallelism is a valid limit for xargs
5  if [[ ! ${1:-} =~ ^[1-9][0-9]*$ ]]; then
6    echo "queue_start: size must be a positive integer, got '${1:-}'" >&2
7    return 1
8  fi
9

10  # Close this shell's copy (not the parent's) of an inherited or earlier
11  # command queue's file descriptor
12  if [[ -n ${_queue_fd:-} ]]; then
13    eval "exec ${_queue_fd}>&-"
14    unset _queue_fd _queue_pid
15  fi
16

17  # Find the first unused fd from 3 to 9 (zsh's redirect max)
18  local fd=3
19  while { : <&"${fd}"; } 2>/dev/null; do fd=$(( fd + 1 )); done
20  if [[ ${fd} -gt 9 ]]; then
21    echo "queue_start: no free fd from 3 to 9" >&2
22    return 1
23  fi
24

25  # Make a FIFO queue file in a private temporary directory
26  local dir
27  dir=$(mktemp -d "${TMPDIR:-/tmp}/queue.XXXXXX")
28  mkfifo "${dir}/queue"
29

30  # Find the shell to execute commands (the current shell)
31  local shell
32  shell=$(ps -o comm= -p "$$")
33  shell=${shell#-}
34

35  # Start xargs in the background, reading NUL-separated commands from the
36  # queue file. Job messages are hidden by turning off job control and
37  # discarding the block's stderr. xargs gets the real stderr through a copy on
38  # the queue files's fd, until the file is opened.
39  eval "exec ${fd}>&2"
40  local opts=$-
41  set +m
42  {
43    # shellcheck disable=SC2016
44    xargs -0 -n 1 -P "$1" "${shell}" -c '
45      # Output is captured rather than streamed so that command output stays separate
46      out=$(eval -- "$1" </dev/null 2>&1); rc=$?
47      [ -z "${out}" ] || printf "%s\n" "${out}"
48      # Coerce non-zero exit codes to 1, because a 255 code would stop xargs
49      [ "${rc}" -eq 0 ] || { printf "exit %d: %s\n" "${rc}" "$1" >&2; exit 1;
50    }' _ <"${dir}/queue" 2>&"${fd}" &
51    _queue_pid=$!
52    disown || :
53  } 2>/dev/null
54  if [[ ${opts} == *m* ]]; then set -m; fi
55

56  # Open the queue file for writing, which waits until xargs has opened it for reading
57  # With both ends open, the file can be safely deleted
58  eval "exec ${fd}>\"\${dir}/queue\""
59  _queue_fd=${fd}
60  rm -rf "${dir}"
61

62  # Register an exit handler to terminate this PID's children, ensuring zsh behaves like bash
63  setopt localoptions posixtraps 2>/dev/null || :
64  trap _queue_stop EXIT
65  if [[ $- != *i* ]]; then
66    trap 'exit 129' HUP
67    trap 'exit 130' INT
68    trap 'exit 143' TERM
69  fi
70}
71
      

        72# 在当前 shell 的命令队列中排队一条命令。
73# @param {string} $1 要运行的命令
74queue_submit() {
75  if [[ -z ${_queue_fd:-} ]]; then
76    echo "queue_submit: no command queue started in this shell" >&2
77    return 1
78

      

$ queue_start 2 $ ( queue_submit 'echo "submitted from a subshell"' ) $ queue_submit 'echo "submitted from the main shell"' $ queue_wait submitted from a subshell submitted from the main shell


        
不同的子 shell 可以拥有自己的命令队列:

      

$ ( queue_start 2 for n in 1 2 3 4; do queue_submit "sleep 1; echo first queue, job ${n}"; done queue_wait ) & $ first=$! $ ( queue_start 1 for n in 1 2; do queue_submit "sleep 1; echo second queue, job ${n}"; done queue_wait ) & $ second=$! $ wait "${first}" $ wait "${second}" second queue, job 1 first queue, job 1 first queue, job 2 second queue, job 2 first queue, job 3 first queue, job 4


        
将命令输出的每一行作为待执行命令加入队列:

      

$ queue_start 2 $ while IFS= read -r line; do queue_submit "${line}" done < <(printf 'echo "from command output, line %d"\n' 1 2 3) $ queue_wait from command output, line 1 from command output, line 2 from command output, line 3


        
将文件的每一行作为命令加入队列:

      

$ queue_start 2 $ while IFS= read -r line; do queue_submit "${line}" done < commands.txt $ queue_wait


        
## 已知问题

以下是一些你可能想换用其他编程语言的理由:

- 一个 shell 只能有一个队列。支持多个队列所需的额外代码很复杂,而且在大多数情况下很可能并不需要。子 shell 仍然可以拥有自己的队列。

一个 shell 只能有一个队列。

支持多个队列所需的额外代码很复杂,而且在大多数情况下很可能并不需要。子 shell 仍然可以拥有自己的队列。

- 命令输出有意不进行流式传输。这可以防止大多数命令的输出交错。大于 PIPE_BUF 的输出仍可能交错。

命令输出有意不进行流式传输。

这可以防止大多数命令的输出交错。大于 PIPE_BUF 的输出仍可能交错。

- queue_wait 不返回表示任何作业失败的值。**这需要临时文件或第二个文件描述符,以及一些复杂的代码。

queue_wait 不返回表示任何作业失败的值。**

这需要临时文件或第二个文件描述符,以及一些复杂的代码。

- queue_wait 必须等待 queue_start 之后启动的运行中进程。在 queue_start 之后启动的进程会继承队列写入文件描述符(这正是子 shell 能够工作的原因),并且在写入描述符的每个副本都关闭之前,xargs 不会看到队列的结尾。

queue_wait 必须等待 queue_start 之后启动的运行中进程。

在 queue_start 之后启动的进程会继承队列写入文件描述符(这正是子 shell 能够工作的原因),并且在写入描述符的每个副本都关闭之前,xargs 不会看到队列的结尾。

- _queue_stop 会不加区分地终止后代进程。任何其他后台作业都会被终止,包括在 queue_start 之前启动的作业。

_queue_stop 会不加区分地终止后代进程。

任何其他后台作业都会被终止,包括在 queue_start 之前启动的作业。

## 为什么不直接用 xargs -P?

如果你事先知道所有想要执行的命令,那么解决方案会简单得多:

      

$ printf 'echo "from command output, line %d"\0' 1 2 3 | \ xargs -0 -n 1 -P 2 bash -c from command output, line 1 from command output, line 2 from command output, line 3


        
      

$ tr '\n' '\0' < commands.txt | \ xargs -0 -n 1 -P 2 bash -c


        
GNU parallel 的工作方式类似,不过 macOS 并不自带它:

      

$ printf 'echo "from command output, line %d"\n' 1 2 3 | \ parallel -j 2 from command output, line 1 from command output, line 2 from command output, line 3

The translation is machine-generated and individual terms may be off. Nothing is rewritten or cut; the original is authoritative.

Published by Hacker News
This site only summarises and aggregates; copyright stays with the original author. Headlines and summaries follow the source — where they differ, the original wins.
Read the original ↗