![并行进程:将输出附加到 bash 脚本中的数组](https://linux22.com/image/137573/%E5%B9%B6%E8%A1%8C%E8%BF%9B%E7%A8%8B%EF%BC%9A%E5%B0%86%E8%BE%93%E5%87%BA%E9%99%84%E5%8A%A0%E5%88%B0%20bash%20%E8%84%9A%E6%9C%AC%E4%B8%AD%E7%9A%84%E6%95%B0%E7%BB%84.png)
我有一个 for 循环,其中task
调用了一个函数。每次调用该函数都会返回一个附加到数组中的字符串。我想并行化这个 for 循环。我尝试使用&
但它似乎不起作用。
这是未并行化的代码。
task (){ sleep 1;echo "hello $1"; }
arr=()
for i in {1..3}; do
arr+=("$(task $i)")
done
for i in "${arr[@]}"; do
echo "$i x";
done
输出是:
hello 1 x
hello 2 x
hello 3 x
伟大的!但现在,当我尝试将它与
[...]
for i in {1..3}; do
arr+=("$(task $i)")&
done
wait
[...]
输出为空。
更新#1
关于task
功能:
- 该函数
task
需要一些时间运行,然后输出一个字符串。收集完所有字符串后,另一个 for 循环将循环遍历字符串并执行其他任务。 - 顺序并不重要。输出字符串可以由单行字符串组成,可能包含由空格分隔的多个单词。
答案1
您无法将赋值发送到后台,因为后台进程是 shell 的一个分支,并且对变量的更改在主 shell 中不可见。
但是您可以并行运行一堆任务,将它们全部输出到管道,然后读取输出的任何内容。或者实际上,使用流程替代,以避免在子 shell 中执行管道中的命令的问题(请参阅为什么我的变量在一个“while read”循环中是本地变量,但在另一个看似相似的循环中却不是?)
只要输出是以原子方式写入的单行,它们就不会混合,但可能会重新排序:
$ task() { sleep 1; echo "$1"; }
$ time while read -r line; do arr+=("$line"); done < <(for x in 1 2 3 ; do task "$x" & done)
real 0m1.006s
$ declare -p arr
declare -a arr=([0]="2" [1]="1" [2]="3")
上面将同时运行所有任务。还有GNU并行(-P
在 GNU xargs 中),它专门用于并行运行任务,并且只会同时运行几个任务。并行还会缓冲任务的输出,因此即使任务分段写入行,也不会获得混合数据。
$ mapfile -t arr < <(parallel -j4 bash ./task.sh ::: {a,b,c})
$ declare -p arr
declare -a arr=([0]="a" [1]="b" [2]="c")
(Bashmapfile
在这里将输入行读取到数组中,类似于while read .. arr+=()
上面的循环。)
如上所述运行外部脚本很简单,但实际上您也可以让它运行导出的函数,当然所有任务都在 shell 的独立副本中运行,因此它们将拥有每个变量等的自己的副本。
$ export -f task
$ mapfile -t arr < <(parallel task ::: {a,b,c})
上面的例子恰好保持了a
、b
、 和c
的顺序,但这是一个巧合。使用parallel -k
它来确保输出保持有序。
答案2
一种稍微幼稚但稳健的方法可以在类似 Bourne 的 shell 中移植:
#!/bin/sh
task () {
tid="$1"
printf 'tid %d: Running...\n' "$tid"
sleep "$(( RANDOM % 5 ))"
printf 'tid %d: Done.\n' "$tid"
}
ntasks=10
tid=0
while [ "$tid" -ne "$ntasks" ]; do
tid=$(( tid + 1 ))
printf 'main: Starting task with tid=%d\n' "$tid"
task "$tid" >"output.$tid" 2>&1 &
done
wait
tid=0
while [ "$tid" -ne "$ntasks" ]; do
tid=$(( tid + 1 ))
printf 'main: Processing output from task with tid=%d\n' "$tid"
# do something with "output.$tid"
done
这会在第一个循环中生成任务,然后等待它们完成,然后再处理第二个循环中的输出。如果任务产生大量数据,这将是合适的。
要将运行任务的数量限制为最多 4 个,可以将初始循环更改为
tid=0
while [ "$tid" -ne "$ntasks" ]; do
tid=$(( tid + 1 ))
printf 'main: Starting task with tid=%d\n' "$tid"
task "$tid" >"output.$tid" 2>&1 &
if [ "$(( tid % 4 ))" -eq 0 ]; then
wait
fi
done
答案3
您正在寻找parset
(自 20170422 起成为 GNU Parallel 的一部分)或env_parset
(自 20171222 起可用):
# If you have not run:
# env_parallel --install
# and logged in again, then you can instead run this to activate (env_)parset:
. `which env_parallel.bash`
task (){
echo "hello $1"
sleep 1.$1
perl -e 'print "binary\001\002\n"'
sleep 1.$1
echo output of parallel jobs do not mix
}
env_parset arr task ::: {1..3}
env_parset a,b,c task ::: {1..3}
echo "${arr[1]}" | xxd
echo "$b" | xxd
parset
Bash/Ksh/Zsh(包括数组)、ash/dash(不包括数组)支持。