Bash FORループの並列化


109

GNU Parallelを使用して、次のスクリプト、特に3つの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

回答:


94

なぜあなたはそれらをフォーク(別名、バックグラウンド)しないのですか?

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
                                   ^

バックグラウンドでフォークされたシェルで実行される関数を引き起こします。それは平行です。


6
それは魅力のように働いた。ありがとうございました。このような単純な実装(今ではとても愚かだと感じます!)。
Ravnoor Sギル

8
並行して実行する8つのファイルがあり、コアが4つしかない場合、そのような設定に統合できますか、それともジョブスケジューラが必要ですか?
ラブヌールSギル

6
このコンテキストでは実際には問題ではありません。システムがコアよりもアクティブなプロセスを持っているのは正常です。短いタスク多数ある場合、理想的には、コアの数以下の数またはワーカースレッドによって処理されるキューにフィードします。それがシェルスクリプトで実際に行われる頻度はわかりません(その場合、スレッドではなく、独立したプロセスになります)が、比較的長いタスクではほとんど意味がありません。OSスケジューラーがそれらを処理します。
goldilocks

17
またwait、最後にコマンドを追加して、バックグラウンドジョブがすべて終了するまでマスタースクリプトが終了しないようにすることもできます。
psusi

1
また、同時プロセスの数を制限すると便利です。各プロセスは、約25分間、コアの時間の100%を使用します。これは、多くの人がジョブを実行している16コアの共有サーバー上にあります。スクリプトの23個のコピーを実行する必要があります。それらをすべて同時に実行すると、サーバーが一杯になり、他のすべてのユーザーが1〜2時間使用できなくなります(負荷が30になり、他のすべての処理が遅くなります)。でできると思いますがnice、それが終わるかどうかはわかりません。
naught10115年

150

サンプルタスク

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個を超えるプロセスが実行されないようにすることもできます。ただし、より多くのコードが必要です。

FIFOベースのセマフォを持つNプロセス:

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 /引数なしですべての子を待機します。それは少し無駄になります。パイプベース・セマフォのアプローチはあなたにもっと流暢並行性を提供します(私が使用してきたそのカスタムシェルベースのビルドシステムで一緒に-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
これも完璧に動作します。確かに、1つのプロセスが他のプロセスに干渉することなく機能するrm *.matように変更する必要がありますrm $run".mat"よろしくお願いします
ラブヌールSギル

@RavnoorSGill Stack Exchangeへようこそ!この回答で問題が解決した場合は、横にあるチェックマークをオンにして、承認済みとしてマークしてください。
ジル

7
+1、wait忘れてた。
goldilocks

5
「もの」が大量にある場合、これは大量のプロセスを開始しませんか?正しい数のプロセスのみを同時に開始する方が良いでしょう?
デビッドドリア

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パラレルプロジェクトの「パラレル」が必要になります(sudo apt-get install parallel)。


1
60を超えることは可能ですか?私は十分なファイル記述子がないことを示すエラーをスローします。
チョビー

かっこが原因で構文エラーがスローされている場合は、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で、チュートリアルを1時間歩きますhttp://www.gnu.org/software/parallel/parallel_tutorial.htmlコマンドラインはあなたを愛します。


あなたは非bashシェルを使用している場合もする必要がありますexport SHELL=/bin/bash並列に実行する前に。そうしないと、次のようなエラーが表示されますUnknown command 'myfunc arg'
。– AndrewHarvey

1
@AndrewHarvey:それはシバンの目的ではありませんか?
naught101

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 CPUコアの数にNを追加します。この数のジョブを並行して実行します。計算集中型ジョブの場合、-j +0は、number-of-cpu-coresジョブを同時に実行するので便利です。

-j -N CPUコアの数からNを引きます。この数のジョブを並行して実行します。評価された数が1未満の場合、1が使用されます。--use-cpus-instead-of-coresも参照してください。


1

私の場合、セマフォを使用できません(Windowsのgit-bashを使用しています)。そのため、タスクを開始する前にN個のワーカーにタスクを分割する一般的な方法を思いつきました。

タスクがほぼ同じ時間かかる場合、うまく機能します。欠点は、作業員の1人が仕事の一部を行うのに長い時間がかかる場合、既に終了した他の作業員が役に立たないことです。

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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.