并行化Bash FOR循环


109

我一直在尝试使用GNU Parallel并行化以下脚本,尤其是三个FOR循环实例中的每个实例,但未能做到。FOR循环中包含的4条命令是串行运行的,每个循环大约需要10分钟。

#!/bin/bash

kar='KAR5'
runList='run2 run3 run4'
mkdir normFunc
for run in $runList
do 
  fsl5.0-flirt -in $kar"deformed.nii.gz" -ref normtemp.nii.gz -omat $run".norm1.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
  fsl5.0-flirt -in $run".poststats.nii.gz" -ref $kar"deformed.nii.gz" -omat $run".norm2.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
  fsl5.0-convert_xfm -concat $run".norm1.mat" -omat $run".norm.mat" $run".norm2.mat"
  fsl5.0-flirt -in $run".poststats.nii.gz" -ref normtemp.nii.gz -out $PWD/normFunc/$run".norm.nii.gz" -applyxfm -init $run".norm.mat" -interp trilinear

  rm -f *.mat
done

Answers:


93

您为什么不只是叉(又称背景)呢?

foo () {
    local run=$1
    fsl5.0-flirt -in $kar"deformed.nii.gz" -ref normtemp.nii.gz -omat $run".norm1.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref $kar"deformed.nii.gz" -omat $run".norm2.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-convert_xfm -concat $run".norm1.mat" -omat $run".norm.mat" $run".norm2.mat"
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref normtemp.nii.gz -out $PWD/normFunc/$run".norm.nii.gz" -applyxfm -init $run".norm.mat" -interp trilinear
}

for run in $runList; do foo "$run" & done

如果不清楚,重要的部分在这里:

for run in $runList; do foo "$run" & done
                                   ^

使该函数在后台的分叉shell中执行。这是并行的。


6
那就像一个魅力。谢谢。如此简单的实现(现在让我感到如此愚蠢!)。
拉夫诺尔·吉尔

8
如果我有8个文件可以并行运行,但只有4个内核,那么是否可以将其集成到这样的设置中,或者需要Job Scheduler?
Ravnoor S Gill 2013年

6
在这种情况下,这并不重要;系统具有比核心更多的活动进程是正常的。如果您有许多短任务,理想情况下,您将输入由数量或工作线程<内核数服务的队列。我不知道用shell脚本真正完成的频率(在这种情况下,它们不是线程,它们是独立的进程),但是执行相对较少的长任务将毫无意义。操作系统调度程序将照顾它们。
goldilocks 2013年

17
您可能还想wait在最后添加一个命令,以便在所有后台作业都退出之前,主脚本不会退出。
psusi 2015年

1
我还限制了并发进程的数量,这很有用:我的每个进程都使用内核的100%时间(约25分钟)。这是在具有16个核心的共享服务器上,许多人在其中运行作业。我需要运行该脚本的23个副本。如果我同时运行它们,那么我将淹没服务器,并使其在一两个小时内对其他所有人无用(负载增加到30,其他所有东西都变慢了)。我想这可能与做nice,但我不知道这是否会永远结束..
naught101

149

样本任务

task(){
   sleep 0.5; echo "$1";
}

顺序运行

for thing in a b c d e f g; do 
   task "$thing"
done

平行运行

for thing in a b c d e f g; do 
  task "$thing" &
done

并行运行N批处理

N=4
(
for thing in a b c d e f g; do 
   ((i=i%N)); ((i++==0)) && wait
   task "$thing" & 
done
)

也可以使用FIFO作为信号量,并使用它们来确保尽快产生新进程,并且确保同时运行的进程不超过N个。但这需要更多代码。

N个进程具有基于FIFO的信号灯:

open_sem(){
    mkfifo pipe-$$
    exec 3<>pipe-$$
    rm pipe-$$
    local i=$1
    for((;i>0;i--)); do
        printf %s 000 >&3
    done
}
run_with_lock(){
    local x
    read -u 3 -n 3 x && ((0==x)) || exit $x
    (
     ( "$@"; )
    printf '%.3d' $? >&3
    )&
}

N=4
open_sem $N
for thing in {a..g}; do
    run_with_lock task $thing
done 

4
它上面的一行wait基本上允许所有进程运行,直到命中该nth进程,然后等待所有其他进程完成运行,对吗?
naught101

如果i为零,请致电等待。i零测试后增加。
PSkocik

2
@ naught101是的。waitw /没有arg等待所有孩子。这有点浪费。基于管道的信号量方法为您提供了更流畅的并发性(我已经在基于定制shell的构建系统中使用它并成功-nt/ -ot进行了一段时间的检查)
PSkocik

1
@ BeowulfNode42您不必退出。只要在任务进程退出/崩溃后将状态(或具有该字节长度的内容)写回到fifo,任务的返回状态就不会损害信号量的一致性。
PSkocik

1
仅供参考,该mkfifo pipe-$$命令需要对当前目录具有适当的写访问权限。因此,我宁愿指定完整路径,例如/tmp/pipe-$$它最可能具有当前用户可用的写访问权限,而不是依赖于当前目录是什么。是替换的所有3次出现pipe-$$
BeowulfNode42

65
for stuff in things
do
( something
  with
  stuff ) &
done
wait # for all the something with stuff

它是否真的有效取决于您的命令;我对他们不熟悉。的rm *.mat,如果它在并行运行看起来有点容易产生矛盾?


2
这也运行得很好。没错,我将不得不更改rm *.matrm $run".mat"使其能够正常工作,而一个过程不会干扰另一个过程。谢谢
Ravnoor S Gill 2013年

@RavnoorSGill欢迎来到Stack Exchange!如果此答案解决了您的问题,请勾选它旁边的对勾,将其标记为已接受
吉尔斯2013年

7
wait我为+1 了。
goldilocks 2013年

5
如果有大量的“事物”,这将不会启动大量的过程吗?最好同时启动许多相同的进程,对吗?
David Doria 2015年

1
非常有用的提示!在这种情况下如何设置线程数?
张大东

30
for stuff in things
do
sem -j+0 ( something
  with
  stuff )
done
sem --wait

这将使用信号量,并行化与可用核心数一样多的迭代(-j +0表示您将并行化N + 0个作业,其中N是可用核数)。

sem --wait告诉您等待直到for循环中的所有迭代都终止执行,然后再执行连续的代码行。

注意:您将需要GNU并行项目中的 “ parallel” (sudo apt-get install parallel)。


1
有可能超过60岁吗?我的抛出一个错误,指出文件描述符不足。
chovy

如果这也是由于括号引起的语法错误,请查看moritzschaefer 的答案
尼古拉

10

我经常使用的一种非常简单的方法:

cat "args" | xargs -P $NUM_PARALLEL command

这将运行命令,并并行传递“ args”文件的每一行,同时最多运行$ NUM_PARALLEL。

如果需要在不同位置替换输入参数,也可以查看xargs的-I选项。


6

似乎fsl作业彼此依赖,因此这4个作业无法并行运行。但是,这些运行可以并行运行。

使bash函数运行一次并并行运行该函数:

#!/bin/bash

myfunc() {
    run=$1
    kar='KAR5'
    mkdir normFunc
    fsl5.0-flirt -in $kar"deformed.nii.gz" -ref normtemp.nii.gz -omat $run".norm1.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref $kar"deformed.nii.gz" -omat $run".norm2.mat" -bins 256 -cost corratio -searchrx -90 90 -searchry -90 90 -searchrz -90 90 -dof 12 
    fsl5.0-convert_xfm -concat $run".norm1.mat" -omat $run".norm.mat" $run".norm2.mat"
    fsl5.0-flirt -in $run".poststats.nii.gz" -ref normtemp.nii.gz -out $PWD/normFunc/$run".norm.nii.gz" -applyxfm -init $run".norm.mat" -interp trilinear
}

export -f myfunc
parallel myfunc ::: run2 run3 run4

要了解更多信息,请观看介绍性视频:https : //www.youtube.com/playlist? list =PL284C9FF2488BC6D1并花一个小时浏览教程http://www.gnu.org/software/parallel/parallel_tutorial.html您的命令线会爱你的。


如果您使用的是非bash shell,则还需要export SHELL=/bin/bash在并行运行之前。否则,您会收到类似以下的错误:Unknown command 'myfunc arg'
AndrewHarvey

1
@AndrewHarvey:那不是shebang的目的吗?
naught101'2015-11-26

5

最大N进程并发中的并行执行

#!/bin/bash

N=4

for i in {a..z}; do
    (
        # .. do your stuff here
        echo "starting task $i.."
        sleep $(( (RANDOM % 3) + 1))
    ) &

    # allow only to execute $N jobs in parallel
    if [[ $(jobs -r -p | wc -l) -gt $N ]]; then
        # wait only for first job
        wait -n
    fi

done

# wait for pending jobs
wait

echo "all done"

3

我非常喜欢@lev的答案,因为它以一种非常简单的方式提供了对最大进程数的控制。但是,如手册中所述,sem不适用于方括号。

for stuff in things
do
sem -j +0 "something; \
  with; \
  stuff"
done
sem --wait

做这份工作。

-j + N将N加到CPU内核数上。并行运行这么多作业。对于计算密集型作业,-j +0很有用,因为它将同时运行cpu-cores作业。

-j -N从CPU内核数中减去N。并行运行这么多作业。如果评估的数字小于1,则将使用1。另请参见--use-cpus-instead-of-cores。


1

就我而言,我不能使用信号量(我在Windows上处于git-bash中),因此我想出了一种通用方法,可以在N个工作程序开始之前将其分配给N个工作程序。

如果任务花费大致相同的时间,则效果很好。缺点是,如果其中一名工人花费很长时间来完成其工作,那么其他已经完成的工人将无济于事。

将工作分配给N个工人(每个核心1个)

# array of assets, assuming at least 1 item exists
listAssets=( {a..z} ) # example: a b c d .. z
# listAssets=( ~/"path with spaces/"*.txt ) # could be file paths

# replace with your task
task() { # $1 = idWorker, $2 = asset
  echo "Worker $1: Asset '$2' START!"
  # simulating a task that randomly takes 3-6 seconds
  sleep $(( ($RANDOM % 4) + 3 ))
  echo "    Worker $1: Asset '$2' OK!"
}

nVirtualCores=$(nproc --all)
nWorkers=$(( $nVirtualCores * 1 )) # I want 1 process per core

worker() { # $1 = idWorker
  echo "Worker $1 GO!"
  idAsset=0
  for asset in "${listAssets[@]}"; do
    # split assets among workers (using modulo); each worker will go through
    # the list and select the asset only if it belongs to that worker
    (( idAsset % nWorkers == $1 )) && task $1 "$asset"
    (( idAsset++ ))
  done
  echo "    Worker $1 ALL DONE!"
}

for (( idWorker=0; idWorker<nWorkers; idWorker++ )); do
  # start workers in parallel, use 1 process for each
  worker $idWorker &
done
wait # until all workers are done

0

@PSkocik的解决方案遇到了麻烦。我的系统没有GNU Parallel作为软件包,sem在我手动构建和运行它时抛出了异常。然后,我也尝试了FIFO信号量示例,该示例还引发了其他一些有关通信的错误。

@eyeApps 建议使用xargs,但我不知道如何使其与复杂的用例一起使用(欢迎使用示例)。

这是我的并行作业解决方案,该作业可以同时处理N_jobs_set_max_parallel以下人员配置的作业:

_lib_jobs.sh:

function _jobs_get_count_e {
   jobs -r | wc -l | tr -d " "
}

function _jobs_set_max_parallel {
   g_jobs_max_jobs=$1
}

function _jobs_get_max_parallel_e {
   [[ $g_jobs_max_jobs ]] && {
      echo $g_jobs_max_jobs

      echo 0
   }

   echo 1
}

function _jobs_is_parallel_available_r() {
   (( $(_jobs_get_count_e) < $g_jobs_max_jobs )) &&
      return 0

   return 1
}

function _jobs_wait_parallel() {
   # Sleep between available jobs
   while true; do
      _jobs_is_parallel_available_r &&
         break

      sleep 0.1s
   done
}

function _jobs_wait() {
   wait
}

用法示例:

#!/bin/bash

source "_lib_jobs.sh"

_jobs_set_max_parallel 3

# Run 10 jobs in parallel with varying amounts of work
for a in {1..10}; do
   _jobs_wait_parallel

   # Sleep between 1-2 seconds to simulate busy work
   sleep_delay=$(echo "scale=1; $(shuf -i 10-20 -n 1)/10" | bc -l)

   ( ### ASYNC
   echo $a
   sleep ${sleep_delay}s
   ) &
done

# Visualize jobs
while true; do
   n_jobs=$(_jobs_get_count_e)

   [[ $n_jobs = 0 ]] &&
      break

   sleep 0.1s
done
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.