プロセスの最大数でBashスクリプトを並列化する


86

Bashにループがあるとしましょう:

for foo in `some-command`
do
   do-something $foo
done

do-somethingCPUにバインドされており、光沢のある4コアプロセッサがあります。do-something一度に4秒まで走れるようにしたいと思います。

素朴なアプローチは次のようです。

for foo in `some-command`
do
   do-something $foo &
done

これが実行され、すべて do-somethingを一度に秒が、カップルの欠点があり、主に行う-何かも行っていくつかの重要なI / Oがあり、そのすべてのビットを遅くするかもしれません一度に。もう1つの問題は、このコードブロックがすぐに返されるため、すべてdo-somethingが終了したときに他の作業を行う方法がないことです。

常にXがdo-something同時に実行されるように、このループをどのように記述しますか?


2
サイドノードとして、プリミティブのbashにmakeの-jオプションを追加することを夢見ていました。常に機能するとは限りませんが、ループの本体が反復ごとに一意の処理を実行することがわかっている単純なケースでは、「for -j4 ...」とだけ言うのはかなりクリーンです。
くつろぐ

1
stackoverflow.com/questions/1537956/…への相互参照。パフォーマンスの問題を軽減し、サブプロセスのグループを分離しておくことができるbashソリューションについて。
paxdiablo 2009年

1
私のソリューションをお勧めしますstackoverflow.com/a/28965927/340581–
タトル

回答:


62

やりたいことに応じて、xargsも役立ちます(ここでは、pdf2psを使用したドキュメントの変換)。

cpus=$( ls -d /sys/devices/system/cpu/cpu[[:digit:]]* | wc -w )

find . -name \*.pdf | xargs --max-args=1 --max-procs=$cpus  pdf2ps

ドキュメントから:

--max-procs=max-procs
-P max-procs
       Run up to max-procs processes at a time; the default is 1.
       If max-procs is 0, xargs will run as many processes as  possible  at  a
       time.  Use the -n option with -P; otherwise chances are that only one
       exec will be done.

9
私の意見では、この方法が最も洗練されたソリューションです。私は被害妄想だから、私はいつものように使用することを除くfind [...] -print0xargs -0
amphetamachine

7
cpus=$(getconf _NPROCESSORS_ONLN)
mr.spuratic 2013

1
マニュアルから、--max-procs=0できるだけ多くのプロセスを取得するために使用してみませんか?
EverythingRightPlace

@EverythingRightPlace、質問は、使用可能なプロセッサより多くのプロセスを明示的に要求しません。 --max-procs=0質問者の試みに似ています(引数と同じ数のプロセスを開始します)。
Toby Speight 2016

39

GNU Parallel http://www.gnu.org/software/parallel/を使用すると、次のように記述できます。

some-command | parallel do-something

GNU Parallelは、リモートコンピューターでのジョブの実行もサポートしています。これにより、リモートコンピューターのCPUコアごとに1つ実行されます。コアの数が異なっていても、次のようになります。

some-command | parallel -S server1,server2 do-something

より高度な例:ここでは、my_scriptを実行するファイルのリストを示します。ファイルの拡張子は(多分.jpeg)です。my_scriptの出力をbasename.out内のファイルの横に配置する必要があります(例:foo.jpeg-> foo.out)。my_scriptは、コンピューターのコアごとに1回実行し、ローカルコンピューターでも実行します。リモートコンピューターの場合、ファイルを処理して特定のコンピューターに転送する必要があります。my_scriptが終了したら、foo.outを転送して戻し、foo.jpegとfoo.outをリモートコンピューターから削除します。

cat list_of_files | \
parallel --trc {.}.out -S server1,server2,: \
"my_script {} > {.}.out"

GNU Parallelは、各ジョブからの出力が混ざらないようにするため、出力を別のプログラムの入力として使用できます。

some-command | parallel do-something | postprocess

その他の例については、ビデオを参照してください:https//www.youtube.com/playlist?list = PL284C9FF2488BC6D1


1
これは、findコマンドを使用してファイルリストを生成するときに非常に便利です。これは、ファイル名内にスペースが存在する場合の問題を防ぐだけでなく、for i in ...; dofindでもfind -name \*.extension1 -or -name \*.extension2GNUparallelの{。}が非常にうまく処理できるためです。
Leo Izen

catもちろん、プラス1は役に立ちません
トリプリー2016

@tripleee Re:猫の無駄な使用。oletange.blogspot.dk/2013/10/useless-use-of-cat.html
Ole

ああ、それはあなたです!ちなみに、そのブログのリンクを更新してもらえますか?partmaps.orgの場所は残念ながら死んでいますが、Ikiリダイレクタは引き続き機能するはずです。
トリプリー2016

22
maxjobs = 4
並列化(){
        while [$#-gt 0]; 行う
                jobcnt =( `jobs -p`)
                if [$ {#jobcnt [@]} -lt $ maxjobs]; その後
                        何か$ 1&
                        シフト  
                そうしないと
                        睡眠1
                fi
        完了
        待つ
}

arg1 arg2 "5argsを3番目のジョブ" arg4 ..に並列化します。

10
いくつかあります実現深刻な引数にスペースを必要とするすべてのジョブがひどく失敗しますので、ここで起こっunderquotingが、さらに、このスクリプトは、maxjobsで許可されているよりも多くのジョブが要求された場合、一部のジョブが終了するのを待っている間、CPUを有効に消費します。
lhunath 2009年

1
また、これは、スクリプトがジョブに関して他に何もしていないことを前提としていることにも注意してください。もしそうなら、それはそれらをmaxjobsにも数えます。
lhunath 2009年

1
「jobs-pr」を使用して、実行中のジョブに制限することをお勧めします。
amphetamachine

1
すでに実行されているdo-somethingコマンドが終了するのを待つ間、whileループが中断せずに繰り返されないようにするsleepコマンドが追加されました。そうしないと、このループは基本的にCPUコアの1つを占有します。これは@lhunathの懸念にも対処します。
euphoria83

12

ここでは、.bashrcに挿入して、日常の1つのライナーに使用できる代替ソリューションを示します。

function pwait() {
    while [ $(jobs -p | wc -l) -ge $1 ]; do
        sleep 1
    done
}

これを使用するには&、ジョブとpwait呼び出しの後に行うだけで、パラメーターは並列プロセスの数を示します。

for i in *; do
    do_something $i &
    pwait 10
done

waitの出力を待つのではなく、使用する方が良いでしょうがjobs -p、すべてではなく、指定されたジョブのいずれかが終了するまで待つという明白な解決策はないようです。


11

単純なbashの代わりに、Makefileを使用して、同時ジョブの数を指定します。make -jXここで、Xは一度に実行するジョブの数です。

または、wait( " man wait")を使用することもできます。複数の子プロセスを起動して呼び出しますwait。子プロセスが終了すると終了します。

maxjobs = 10

foreach line in `cat file.txt` {
 jobsrunning = 0
 while jobsrunning < maxjobs {
  do job &
  jobsrunning += 1
 }
wait
}

job ( ){
...
}

ジョブの結果を保存する必要がある場合は、その結果を変数に割り当てます。wait変数に何が含まれているかを確認した後。


1
これに感謝します。コードが完成していなくても、私が仕事で抱えている問題に対する答えが得られます。
gerikson 2008

唯一の問題は、フォアグラウンドスクリプト(ループのあるスクリプト)を
Girardi 2018年

8

ループを書き直す代わりに、並列化ユーティリティを試してみてください。私はxjobsの大ファンです。私はいつもxjobsを使用して、ネットワーク全体でファイルを一括コピーします。通常、新しいデータベースサーバーをセットアップするときです。 http://www.maier-komor.de/xjobs.html


7

makeコマンドに精通している場合は、ほとんどの場合、実行するコマンドのリストをmakefileとして表現できます。たとえば、それぞれが* .outputを生成するファイル* .inputに対して$ SOME_COMMANDを実行する必要がある場合は、makefileを使用できます。

INPUT = a.input b.input
OUTPUT = $(INPUT:.input = .output)

%。出力入力
    $(SOME_COMMAND)$ <$ @

すべて:$(出力)

そしてただ走る

make -j <NUMBER>

最大でNUMBERコマンドを並行して実行します。


6

これを正しく行うことbashはおそらく不可能ですが、セミライトをかなり簡単に行うことができます。 bstark権利の公正な概算を与えたが、彼には以下の欠陥がある:

  • 単語の分割:引数にスペース、タブ、改行、星、疑問符のいずれかの文字を使用するジョブを渡すことはできません。そうした場合、おそらく予期せずに物事が壊れます。
  • スクリプトの残りの部分に依存して、バックグラウンドを設定しません。そうした場合、または後でスニペットのためにバックグラウンドジョブの使用が許可されなかったことを忘れたためにバックグラウンドで送信されるスクリプトに何かを追加すると、問題が発生します。

これらの欠陥がない別の近似は次のとおりです。

scheduleAll() {
    local job i=0 max=4 pids=()

    for job; do
        (( ++i % max == 0 )) && {
            wait "${pids[@]}"
            pids=()
        }

        bash -c "$job" & pids+=("$!")
    done

    wait "${pids[@]}"
}

これは、終了時に各ジョブの終了コードもチェックするように簡単に適応できるため、ジョブが失敗した場合にユーザーに警告したり、失敗したジョブの数などにscheduleAll応じて終了コードを設定したりできます。

このコードの問題はそれだけです:

  • 一度に4つ(この場合)のジョブをスケジュールし、4つすべてが終了するのを待ちます。一部のジョブは他のジョブよりも早く実行される可能性があり、4つのジョブの次のバッチは、前のバッチの最長のものが実行されるまで待機します。

この最後の問題をkill -0処理するソリューションは、の代わりにプロセスのいずれかが消えたかどうかをポーリングしwait、次のジョブをスケジュールするために使用する必要があります。ただし、これにより、小さな新しい問題が発生します。ジョブが終了してから、ジョブが終了したkill -0かどうかを確認するまでの間に競合状態が発生します。ジョブが終了し、システム上の別のプロセスが同時に起動し、終了したばかりのジョブのランダムなPIDを取得すると、ジョブが終了したkill -0ことに気付かず、再び問題が発生します。

で完璧な解決策は不可能bashです。


3

bashの関数:

parallel ()
{
    awk "BEGIN{print \"all: ALL_TARGETS\\n\"}{print \"TARGET_\"NR\":\\n\\t@-\"\$0\"\\n\"}END{printf \"ALL_TARGETS:\";for(i=1;i<=NR;i++){printf \" TARGET_%d\",i};print\"\\n\"}" | make $@ -f - all
}

使用:

cat my_commands | parallel -j 4

の使用make -jは巧妙ですが、説明がなく、書き込み専用のAwkコードのブロブがあるため、賛成することは控えています。
トリプリー2016

2

私が取り組んでいるプロジェクトでは、waitコマンドを使用して並列シェル(実際にはksh)プロセスを制御しています。IOに関する懸念に対処するために、最新のOSでは、並列実行によって実際に効率が向上する可能性があります。すべてのプロセスがディスク上の同じブロックを読み取っている場合、最初のプロセスのみが物理ハードウェアにアクセスする必要があります。他のプロセスは、多くの場合、メモリ内のOSのディスクキャッシュからブロックを取得できます。明らかに、メモリからの読み取りは、ディスクからの読み取りよりも数桁高速です。また、この利点はコーディングの変更を必要としません。


1

これはほとんどの目的には十分かもしれませんが、最適ではありません。

#!/bin/bash

n=0
maxjobs=10

for i in *.m4a ; do
    # ( DO SOMETHING ) &

    # limit jobs
    if (( $(($((++n)) % $maxjobs)) == 0 )) ; then
        wait # wait until all have finished (not optimal, but most times good enough)
        echo $n wait
    fi
done

1

これが私がbashスクリプトでこの問題を解決する方法です:

 #! /bin/bash

 MAX_JOBS=32

 FILE_LIST=($(cat ${1}))

 echo Length ${#FILE_LIST[@]}

 for ((INDEX=0; INDEX < ${#FILE_LIST[@]}; INDEX=$((${INDEX}+${MAX_JOBS})) ));
 do
     JOBS_RUNNING=0
     while ((JOBS_RUNNING < MAX_JOBS))
     do
         I=$((${INDEX}+${JOBS_RUNNING}))
         FILE=${FILE_LIST[${I}]}
         if [ "$FILE" != "" ];then
             echo $JOBS_RUNNING $FILE
             ./M22Checker ${FILE} &
         else
             echo $JOBS_RUNNING NULL &
         fi
         JOBS_RUNNING=$((JOBS_RUNNING+1))
     done
     wait
 done

1

ここでのパーティーには本当に遅れていますが、別の解決策があります。

多くのソリューションは、コマンド内のスペース/特殊文字を処理しない、N個のジョブを常に実行し続ける、ビジーループでCPUを消費する、または外部依存関係(GNUなどparallel)に依存します。

死んだ/ゾンビプロセス処理のためのインスピレーション、ここでは純粋なbashのソリューションです:

function run_parallel_jobs {
    local concurrent_max=$1
    local callback=$2
    local cmds=("${@:3}")
    local jobs=( )

    while [[ "${#cmds[@]}" -gt 0 ]] || [[ "${#jobs[@]}" -gt 0 ]]; do
        while [[ "${#jobs[@]}" -lt $concurrent_max ]] && [[ "${#cmds[@]}" -gt 0 ]]; do
            local cmd="${cmds[0]}"
            cmds=("${cmds[@]:1}")

            bash -c "$cmd" &
            jobs+=($!)
        done

        local job="${jobs[0]}"
        jobs=("${jobs[@]:1}")

        local state="$(ps -p $job -o state= 2>/dev/null)"

        if [[ "$state" == "D" ]] || [[ "$state" == "Z" ]]; then
            $callback $job
        else
            wait $job
            $callback $job $?
        fi
    done
}

そしてサンプルの使用法:

function job_done {
    if [[ $# -lt 2 ]]; then
        echo "PID $1 died unexpectedly"
    else
        echo "PID $1 exited $2"
    fi
}

cmds=( \
    "echo 1; sleep 1; exit 1" \
    "echo 2; sleep 2; exit 2" \
    "echo 3; sleep 3; exit 3" \
    "echo 4; sleep 4; exit 4" \
    "echo 5; sleep 5; exit 5" \
)

# cpus="$(getconf _NPROCESSORS_ONLN)"
cpus=3
run_parallel_jobs $cpus "job_done" "${cmds[@]}"

出力:

1
2
3
PID 56712 exited 1
4
PID 56713 exited 2
5
PID 56714 exited 3
PID 56720 exited 4
PID 56724 exited 5

プロセスごとの出力処理で$$は、次のようにファイルにログを記録できます。

function job_done {
    cat "$1.log"
}

cmds=( \
    "echo 1 \$\$ >\$\$.log" \
    "echo 2 \$\$ >\$\$.log" \
)

run_parallel_jobs 2 "job_done" "${cmds[@]}"

出力:

1 56871
2 56872

0

単純なネストされたforループを使用できます(以下のNとMを適切な整数に置き換えてください)。

for i in {1..N}; do
  (for j in {1..M}; do do_something; done & );
done

これにより、do_somethingがMラウンドでN * M回実行され、各ラウンドでN個のジョブが並行して実行されます。NをCPUの数と等しくすることができます。


0

常に特定の数のプロセスを実行し続け、エラーを追跡し、中断できない/ゾンビプロセスを処理するための私のソリューション:

function log {
    echo "$1"
}

# Take a list of commands to run, runs them sequentially with numberOfProcesses commands simultaneously runs
# Returns the number of non zero exit codes from commands
function ParallelExec {
    local numberOfProcesses="${1}" # Number of simultaneous commands to run
    local commandsArg="${2}" # Semi-colon separated list of commands

    local pid
    local runningPids=0
    local counter=0
    local commandsArray
    local pidsArray
    local newPidsArray
    local retval
    local retvalAll=0
    local pidState
    local commandsArrayPid

    IFS=';' read -r -a commandsArray <<< "$commandsArg"

    log "Runnning ${#commandsArray[@]} commands in $numberOfProcesses simultaneous processes."

    while [ $counter -lt "${#commandsArray[@]}" ] || [ ${#pidsArray[@]} -gt 0 ]; do

        while [ $counter -lt "${#commandsArray[@]}" ] && [ ${#pidsArray[@]} -lt $numberOfProcesses ]; do
            log "Running command [${commandsArray[$counter]}]."
            eval "${commandsArray[$counter]}" &
            pid=$!
            pidsArray+=($pid)
            commandsArrayPid[$pid]="${commandsArray[$counter]}"
            counter=$((counter+1))
        done


        newPidsArray=()
        for pid in "${pidsArray[@]}"; do
            # Handle uninterruptible sleep state or zombies by ommiting them from running process array (How to kill that is already dead ? :)
            if kill -0 $pid > /dev/null 2>&1; then
                pidState=$(ps -p$pid -o state= 2 > /dev/null)
                if [ "$pidState" != "D" ] && [ "$pidState" != "Z" ]; then
                    newPidsArray+=($pid)
                fi
            else
                # pid is dead, get it's exit code from wait command
                wait $pid
                retval=$?
                if [ $retval -ne 0 ]; then
                    log "Command [${commandsArrayPid[$pid]}] failed with exit code [$retval]."
                    retvalAll=$((retvalAll+1))
                fi
            fi
        done
        pidsArray=("${newPidsArray[@]}")

        # Add a trivial sleep time so bash won't eat all CPU
        sleep .05
    done

    return $retvalAll
}

使用法:

cmds="du -csh /var;du -csh /tmp;sleep 3;du -csh /root;sleep 10; du -csh /home"

# Execute 2 processes at a time
ParallelExec 2 "$cmds"

# Execute 4 processes at a time
ParallelExec 4 "$cmds"

-1

$ DOMAINS = "コマンド内のいくつかのドメインのリスト"、foo in some-command do

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

完了

Ndomains =echo $DOMAINS |wc -w

for i in $(seq 1 1 $ Ndomains)do echo "wait for $ {job [$ i]}" wait "$ {job [$ i]}" done

この概念では、並列化に使用できます。重要なのは、evalの最後の行が「&」であり、コマンドをバックグラウンドに配置することです。

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