编写 Bash 代码的并行版本时出现问题

编写 Bash 代码的并行版本时出现问题

我正在尝试并行化我的示例 Bash 脚本,并尝试了诸如&和 之类的命令wait。请告诉我什么是使其并行的有效方法

我当前的代码对于 reg2 变量中的有限条目运行良好。但我在 reg2 变量中有数百万个条目。所以我想让最外层的循环平行。并行化代码后获得相同的输出,即 0,1,2,:,3,4,:,5,6

#!/bin/bash

# array1=$1
# array2=($2)
# reg2=($3)

array1=('bam1' 'bam2' 'bam3' 'bam4' 'bam5' 'bam6' 'bam7')
array2=('cell1' 'cell1' 'cell1' 'cell2' 'cell2' 'cell3' 'cell3')
reg2=('chr1:10484-10572' 'chr1:10589-10632' 'chr1:10636-10661' 'chr1:10665-10690' 'chr1:10694-10719') 

start=`date +%s.%N`

l=${#reg2[@]} # number of regions is 30 million on real data
reg_cov=()
j=0
for r in ${reg2[@]}; do
    
    (cov_array=()
    old_array2_element=${array2[0]}
    
    for i in ${!array1[*]}; do
      new_array2_element=${array2[$i]}
      
      if [[ "$new_array2_element" != "$old_array2_element" ]]; then
        cov_array+=(":")
        old_array2_element=$new_array2_element
      fi
      cov_array+=($i) # in actual code this step takes 4-5 seconds to process
      sleep 2
    done
    
    
    reg_cov+=($(IFS=, ; echo "${cov_array[*]}"))  )
    wait
    
    ((j++)) 
    echo "$j/$l"
done

#echo ${reg_cov[@]}
cov=()
cov+=(${reg_cov[@]})
echo $cov


end=`date +%s.%N`; runtime=$( echo "$end - $start" | bc -l ); runtime=${runtime%.*}; hours=$((runtime / 3600)); minutes=$(( (runtime % 3600) / 60 )); seconds=$(( (runtime % 3600) % 60 ))
echo "==> completed Runtime: $hours:$minutes:$seconds (hh:mm:ss)"

答案1

正如评论中提到的,由于性能原因,对于数百万个项目,您可能应该使用 Bash 以外的几乎任何东西。 Shell 通常速度不是很快,而 Bash 是最慢的之一。我也不认为它在处理大型数组时会非常有效,但我认为我没有特别见过这方面的测试。

您的脚本还会在外循环的每次迭代中启动两个子 shell,一个从 at 开始,(cov_array=()另一个在命令替换 中开始$(IFS=, ; echo "${cov_array[*]}")。在 Bash 中,这些将涉及分叉一个子进程,如果适度的话,这并没有那么糟糕,但重复数百万次,就会开始付出一些代价。

话又说回来,如果每个项目需要 4-5 秒来处理,那么子进程的开销可能就不那么重要了。 (您还需要大约 10 天的时间来处理 3 M 条目,并进行 16 倍并行化,仅每个项目的几秒钟。或者是每个项目 4-5 秒循环迭代?因此,该时间乘以 中的项目数array1。如上所述,有 7 件商品,大约是三月初。您可能需要考虑是否可以优化最里面的步骤。)

另请注意,就目前情况而言,您的脚本不会打印任何有用的内容:对的赋值reg_cov位于子 shell 中,因此最终主程序将看不到它,并且不会有输出。并行运行多个任务还需要运行多个不同的进程,并且如果您需要的话,您必须做出安排将结果移回主进程。它不会自动发生,至少在 shell 中不会。或者只是从文件中读取并打印到文件。

然后还有一些相对较小的问题,${reg2[@]}会使数组元素进行分词:您应该使用"${reg2[@]}"它。看起来也for i in ${!array1[*]}有点奇怪,因为您实际上没有array1在任何地方使用:在我看来,您可以直接循环遍历值array2。并且仅打印索引处的echo $cov元素,要打印整个数组,您需要or打印整个内容。0covecho "${cov[@]}"echo "${cov[*]}"


根据您的任务最里面的步骤实际执行的操作以及项目的来源reg2,我建议查看例如 GNU Parallel。它可以从文件中读取输入,为每个项目运行一个进程,并以合理的顺序收集输出。


也就是说,如果您想在 shell 中并行化某些内容,前面的帖子中有一些解决方案,请参阅例如 并行化 Bash FOR 循环

答案2

确实很难看出您想要做什么,但让我们假设您有一个名为 的包含 3000 万行的文件reg2.txt,并且您想要为每一行运行一个 bash 函数:

doit() {
  reg2="$1"
  echo do stuff with "$reg2"
  array1=('bam1' 'bam2' 'bam3' 'bam4' 'bam5' 'bam6' 'bam7')
  for i in ${!array1[*]}; do
     printf "$i "
  done
  echo
}

您应该确保doit chr1:10484-10572做正确的事情。

当它起作用时,您可以执行以下操作:

export -f doit
cat reg2.txt | parallel doit

这将为doitreg2.txt 中的每一行运行,每个 CPU 线程并行运行 1 个作业。

答案3

如果我理解正确的话,外层循环大约有 3000 万次迭代,内层循环大约有 7 次迭代,最里面的计算需要 4-5 秒。总共需要 29.9 年才能完成!在最好的情况下,跨 64 个核心并行化这种方法可能会将执行时间减少到约 5.6 个月,但这仍然不切实际。

最好的方法是首先优化 4-5 秒计算的代码(未显示)。

相关内容