在bash脚本中并行运行数组中的每个元素

时间:2017-04-09 15:13:36

标签: bash shell

假设我有一个看起来像这样的bash脚本:

array=( 1 2 3 4 5 6 )

for each in "${array[@]}"
do
  echo "$each"

  command --arg1 $each

done

如果我想并行运行循环中的所有内容,我可以将command --arg1 $each更改为command --arg1 $each &

但现在让我们说我想取command --arg1 $each的结果并对这些结果做点什么:

array=( 1 2 3 4 5 6 )
for each in "${array[@]}"
do
  echo "$each"

  lags=($(command --arg1 $each)

  lngth_lags=${#lags[*]}

  for (( i=1; i<=$(( $lngth_lags -1 )); i++))
  do

    result=${lags[$i]}
    echo -e "$timestamp\t$result" >> $log_file
    echo "result piped"

  done

done

如果我只是在&的末尾添加command --arg1 $each,那么command --arg1 $each之后的所有内容都将在没有command --arg1 $each完成的情况下运行。我该如何防止这种情况发生?另外,我如何限制循环可以占用的线程数量?

基本上,此块应与1,2,3,4,5,6

并行运行
  echo "$each"

  lags=($(command --arg1 $each)

  lngth_lags=${#lags[*]}

  for (( i=1; i<=$(( $lngth_lags -1 )); i++))
  do

    result=${lags[$i]}
    echo -e "$timestamp\t$result" >> $log_file
    echo "result piped"

  done

----- EDIT --------

以下是原始代码:

#!/bin/bash
export KAFKA_OPTS="-Djava.security.krb5.conf=/etc/krb5.conf -Djava.security.auth.login.config=/etc/kafka/kafka.client.jaas.conf"
IFS=$'\n'
array=($(kafka-consumer-groups --bootstrap-server kafka1:9092 --list --command-config /etc/kafka/client.properties --new-consumer))

lngth=${#array[*]}

echo "array length: " $lngth

timestamp=$(($(date +%s%N)/1000000))

log_time=`date +%Y-%m-%d:%H`

echo "log time: " $log_time

log_file="/home/ec2-user/laglogs/laglog.$log_time.log"

echo "log file: " $log_file

echo "timestamp: " $timestamp

get_lags () {

  echo "$1"

  lags=($(kafka-consumer-groups --bootstrap-server kafka1:9092 --describe  --group $1 --command-config /etc/kafka/client.properties --new-consumer))

  lngth_lags=${#lags[*]}

  for (( i=1; i<=$(( $lngth_lags -1 )); i++))
  do

    result=${lags[$i]}
    echo -e "$timestamp\t$result" >> $log_file
    echo "result piped"

  done
}

for each in "${array[@]}"
do 

   get_lags $each &

done

------编辑2 -----------

尝试以下答案:

#!/bin/bash
export KAFKA_OPTS="-Djava.security.krb5.conf=/etc/krb5.conf -Djava.security.auth.login.config=/etc/kafka/kafka.client.jaas.conf"
IFS=$'\n'
array=($(kafka-consumer-groups --bootstrap-server kafka1:9092 --list --command-config /etc/kafka/client.properties --new-consumer))

lngth=${#array[*]}

echo "array length: " $lngth

timestamp=$(($(date +%s%N)/1000000))

log_time=`date +%Y-%m-%d:%H`

echo "log time: " $log_time

log_file="/home/ec2-user/laglogs/laglog.$log_time.log"

echo "log file: " $log_file

echo "timestamp: " $timestamp

max_proc_count=8

run_for_each() {
  local each=$1
  echo "Processing: $each" >&2
  IFS=$'\n' read -r -d '' -a lags < <(kafka-consumer-groups --bootstrap-server kafka1:9092 --describe --command-config /etc/kafka/client.properties --new-consumer --group "$each" && printf '\0')
  for result in "${lags[@]}"; do
    printf '%(%Y-%m-%dT%H:%M:%S)T\t%s\t%s\n' -1 "$each" "$result"
  done >>"$log_file"
}

export -f run_for_each
export log_file # make log_file visible to subprocesses

printf '%s\0' "${array[@]}" |
  xargs -P "$max_proc_count" -n 1 -0 bash -c 'run_for_each "$@"'

5 个答案:

答案 0 :(得分:2)

方便的做法是将后台代码推送到单独的脚本或导出的函数中。这样xargs可以创建一个新shell,并从其父级访问该函数。 (确保export孩子也需要其他任何变量。

array=( 1 2 3 4 5 6 )
max_proc_count=8
log_file=out.txt

run_for_each() {
  local each=$1
  echo "Processing: $each" >&2
  IFS=$' \t\n' read -r -d '' -a lags < <(yourcommand --arg1 "$each" && printf '\0')
  for result in "${lags[@]}"; do
    printf '%(%Y-%m-%dT%H:%M:%S)T\t%s\t%s\n' -1 "$each" "$result"
  done >>"$log_file"
}

export -f run_for_each
export log_file # make log_file visible to subprocesses

printf '%s\0' "${array[@]}" |
  xargs -P "$max_proc_count" -n 1 -0 bash -c 'run_for_each "$@"'

一些注意事项:

  • 使用echo -e是不好的形式。请参阅the POSIX spec for echo中的“应用程序使用”和“RATIONALE”部分,明确建议使用printf代替(定义-e选项,明确定义比echo不得接受-n以外的任何选项。
  • 我们在日志文件中包含each值,以便稍后从中提取。
  • 您尚未指定yourcommand的输出是以空格分隔,制表符分隔,行分隔还是其他方式。我现在接受所有这些;修改传递给IFS的{​​{1}}的值来品尝。
  • read获取没有printf '%(...)T'等外部工具的时间戳需要bash 4.2或更新版本。如果您认为合适,请替换为您自己的代码。
  • dateread -r -a arrayname < <(...)强大得多。特别是,它避免将发出的值视为globs - 将arrayname=( $(...) )替换为当前目录中的文件列表,或者*替换为Foo[Bar]如果存在该名称的任何文件(或,如果设置了FooBfailglob选项,则在这种情况下触发失败或根本不发出任何值。)
  • 对于整个循环,将stdout重定向到nullglob一次比每次运行log_file一次重定向更有效。请注意,让多个进程同时写入同一个文件只有在所有进程都使用printfO_APPEND将会执行)打开的情况下才会安全,并且如果它们以块的形式写入小到足以单独完成单个系统调用(除非单个>>值非常大,否则可能正在发生)。

答案 1 :(得分:2)

这里有许多长篇大论和理论上的答案,我会尽量保持简单 - 如何使用|(管道)照常连接命令?;)(和GNU parallel,擅长这些类型的任务)。

seq 6 | parallel -j4 "command --arg1 {} | command2 > results/{}"

-j4将根据请求限制线程数(作业)。您不希望从多个作业写入单个文件,每个作业输出一个文件,并在并行处理完成后加入它们。

答案 2 :(得分:0)

您知道如何在单独的进程中执行命令。缺少的部分是如何允许这些进程进行通信,因为单独的进程无法共享变量。

基本上,您必须选择是使用常规文件进行通信,还是进程间通信/ FIFO(仍然归结为使用文件)。

一般方法:

  • 决定如何呈现要执行的任务。您可以将它们作为文件系统上的单独文件,作为可以从中读取的FIFO特殊文件等。这可以是一个简单的操作,即将每个命令写入单独的文件,或将每个命令写入FIFO(一个每行命令)。

  • 在主要流程中,准备描述要执行的任务的文件,或在后台启动一个单独的流程,以便为FIFO提供信息。

  • 然后,仍然在主进程中,在后台启动工作进程(使用&),就像您希望执行并行任务一样(不是每个任务要执行一个)。启动后,使用wait,等待所有进程完成。单独的进程不能共享变量,您必须编写以后需要用来分隔文件的任何输出,或FIFO等。如果使用FIFO,请记住多个进程可以同时写入FIFO,所以使用某种互斥机制(我建议为此目的考虑使用mkdir / rmdir)。

  • 每个工作进程必须获取下一个任务(从文件/ FIFO),执行它,生成输出(到文件/ FIFO),循环直到没有新任务,然后退出。如果使用文件,则需要使用互斥锁“保留”文件,读取文件,然后将其删除以将其标记为已处理。 FIFO不需要这样做。

  • 根据具体情况,您的主进程可能必须等到所有任务完成才能处理输出,或者在某些情况下可能会启动一个工作进程,该进程将检测并处理输出。一旦执行完所有任务,该工作进程必须由主进程停止,或者在所有任务执行完毕并退出时(在主进程被wait编辑时)自行确定。 / p>

这不是详细的代码,但我希望它能让您了解如何处理这样的问题。

答案 3 :(得分:0)

(社区维基回答问题中的OP's proposed self-answer - 现已编辑完毕):

所以这是我可以想到这样做的一种方式,不确定这是否是最有效的方式,而且,我无法控制线程的数量(我认为或进程?),这将使用:

array=( 1 2 3 4 5 6 )

lag_func () {
  echo "$1"

  lags=($(command --arg1 $1)

  lngth_lags=${#lags[*]}

  for (( i=1; i<=$(( $lngth_lags -1 )); i++))
  do
    result=${lags[$i]}
    echo -e "$timestamp\t$result" >> $log_file
    echo "result piped"
  done
}

for each in "${array[@]}"
do
  lag_func $each &
done

答案 4 :(得分:0)

使用GNU Parallel看起来像这样:

array=( 1 2 3 4 5 6 )
parallel -0 --bar --tagstring '{= $_=localtime(time)."\t".$_; =}' \
  command --arg1 {} ::: "${array[@]}" > output

GNU Parallel确保不会混合来自不同作业的输出。

如果您更喜欢混合作业的输出:

parallel -0 --bar --line-buffer --tagstring '{= $_=localtime(time)."\t".$_; =}' \
  command --arg1 {} ::: "${array[@]}" > output-linebuffer

再次,GNU Parallel确保只与整行混合:你不会看到一个作业的半行和另一个作业的半行。

如果数组有点讨厌,它也有效:

array=( "new
line" 'quotes"  '"'" 'echo `do not execute me`')

或者如果命令打印长行半行:

command() {
  echo Input: "$@"
  echo '"  '"'"
  sleep 1
  echo -n 'Half a line '
  sleep 1
  echo other half

  superlong_a=$(perl -e 'print "a"x1000000')
  superlong_b=$(perl -e 'print "b"x1000000')
  echo -n $superlong_a
  sleep 1
  echo $superlong_b
}
export -f command

GNU Parallel努力成为一般解决方案。这是因为我设计了GNU Parallel以关注正确性,并且在保持合理快速的同时,也要努力正确处理角落情况。

GNU Parallel防范竞争条件,并且不会在每行的输出中拆分单词。

array=( $(seq 30) )
max_proc_count=30

command() {
  # If 'a', 'b' and 'c' mix: Very bad                                                         
  perl -e 'print "a"x3000_000," "'
  perl -e 'print "b"x3000_000," "'
  perl -e 'print "c"x3000_000," "'
  echo
}
export -f command

parallel -0 --bar --tagstring '{= $_=localtime(time)."\t".$_; =}' \
  command --arg1 {} ::: "${array[@]}" > parallel.out

# 'abc' should always stay together
# and there should only be a single line per job
cat parallel.out | tr -s abc

如果输出有很多单词,GNU Parallel工作正常:

array=(1)
command() {
  yes "`seq 1000`" | head -c 10M
}
export -f command

parallel -0 --bar --tagstring '{= $_=localtime(time)."\t".$_; =}' \
  command --arg1 {} ::: "${array[@]}" > parallel.out

GNU Parallel不会占用你所有的内存 - 即使输出大于你的RAM:

array=(1)
outputsize=1000M
export outputsize
command() {
    yes "`perl -e 'print \"c\"x30_000'`" | head -c $outputsize
}
export -f command

parallel -0 --bar --tagstring '{= $_=localtime(time)."\t".$_; =}' \
  command --arg1 {} ::: "${array[@]}" > parallel.out