Paralelkan skrip Bash dengan jumlah proses maksimum

87

Katakanlah saya memiliki loop di Bash:

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

do-somethingterikat cpu dan saya memiliki prosesor 4 inti yang bagus dan berkilau. Saya ingin bisa berlari hingga 4 do-somethingsekaligus.

Pendekatan yang naif tampaknya:

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

Ini akan menjalankan semua do-something s sekaligus, tetapi ada beberapa kelemahan, terutama yang melakukan-sesuatu yang juga mungkin memiliki beberapa signifikan I / O yang tampil semua sekaligus mungkin memperlambat sedikit. Masalah lainnya adalah blok kode ini segera kembali, jadi tidak ada cara untuk melakukan pekerjaan lain ketika semua do-somethingsudah selesai.

Bagaimana Anda menulis loop ini sehingga selalu ada X do-somethingyang berjalan sekaligus?

thelsdj
sumber
2
Sebagai sidenode, saya bermimpi menambahkan opsi -j make ke bash untuk primitif. Ini tidak akan selalu berhasil, tetapi untuk beberapa kasus sederhana di mana Anda tahu badan loop akan melakukan sesuatu yang unik untuk setiap iterasi, akan cukup bersih untuk hanya mengatakan "untuk -j 4 ...".
bersantai
1
Referensi silang ke stackoverflow.com/questions/1537956/… untuk solusi bash yang mengurangi masalah kinerja dan memungkinkan grup sub-proses, tetap terpisah.
paxdiablo
1
Saya akan merekomendasikan solusi saya stackoverflow.com/a/28965927/340581
Tuttle

Jawaban:

63

Bergantung pada apa yang ingin Anda lakukan, xargs juga dapat membantu (di sini: mengonversi dokumen dengan pdf2ps):

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

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

Dari dokumen:

--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.
Fritz G. Mehner
sumber
9
Cara ini, menurut saya, adalah solusi paling elegan. Kecuali, karena saya paranoid, saya selalu suka menggunakan find [...] -print0dan xargs -0.
amphetamachine
7
cpus=$(getconf _NPROCESSORS_ONLN)
mr. Spuratic
1
Dari manual, mengapa tidak menggunakan --max-procs=0proses sebanyak mungkin?
EverythingRightPlace
@EverythingRightPlace, pertanyaan secara eksplisit meminta tidak lebih banyak proses daripada prosesor yang tersedia. --max-procs=0lebih seperti upaya penanya (memulai proses sebanyak argumen).
Toby Speight
39

Dengan GNU Parallel http://www.gnu.org/software/parallel/ Anda dapat menulis:

some-command | parallel do-something

GNU Parallel juga mendukung menjalankan pekerjaan pada komputer jarak jauh. Ini akan berjalan satu per inti CPU pada komputer jarak jauh - meskipun mereka memiliki jumlah inti yang berbeda:

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

Contoh yang lebih canggih: Di sini kami membuat daftar file yang kami ingin my_script untuk dijalankan. File memiliki ekstensi (mungkin .jpeg). Kami ingin output dari my_script diletakkan di sebelah file di basename.out (misalnya foo.jpeg -> foo.out). Kami ingin menjalankan my_script sekali untuk setiap inti yang dimiliki komputer dan kami ingin menjalankannya di komputer lokal juga. Untuk komputer jarak jauh kami ingin file yang akan diproses ditransfer ke komputer yang diberikan. Ketika my_script selesai, kami ingin foo.out ditransfer kembali dan kami ingin foo.jpeg dan foo.out dihapus dari komputer jarak jauh:

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

GNU Parallel memastikan keluaran dari setiap pekerjaan tidak tercampur, sehingga Anda dapat menggunakan keluaran sebagai masukan untuk program lain:

some-command | parallel do-something | postprocess

Lihat video untuk lebih banyak contoh: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Ole Tange
sumber
1
Perhatikan bahwa ini sangat berguna saat menggunakan findperintah untuk menghasilkan daftar file, karena tidak hanya mencegah masalah saat ada spasi di dalam nama file yang muncul, for i in ...; dotetapi find juga dapat melakukan hal find -name \*.extension1 -or -name \*.extension2yang dapat ditangani oleh GNU paralel {.} Dengan sangat baik.
Leo Izen
Ditambah 1 cat, tentu saja, tidak berguna.
tripleee
@tripleee Re: Penggunaan kucing yang tidak berguna. Lihat oletange.blogspot.dk/2013/10/useless-use-of-cat.html
Ole Tange
Oh itu kamu! Kebetulan, dapatkah Anda memperbarui tautan di blog itu? Sayangnya lokasi partmaps.org sudah mati, tapi redirector Iki harus terus bekerja.
tripleee
22
maxjobs = 4
parallelize () {
        sementara [$ # -gt 0]; melakukan
                jobcnt = (`pekerjaan -p`)
                jika [$ {# jobcnt [@]} -lt $ maxjobs]; kemudian
                        melakukan sesuatu $ 1 &
                        bergeser  
                lain
                        tidur 1
                fi
        selesai
        Tunggu
}

memparalelkan arg1 arg2 "5 args to third job" arg4 ...
kulit kayu
sumber
10
Sadarilah bahwa ada beberapa underquoting serius yang terjadi di sini sehingga pekerjaan apa pun yang membutuhkan spasi dalam argumen akan gagal; Selain itu, skrip ini akan memakan CPU Anda hidup-hidup sementara menunggu beberapa pekerjaan selesai jika lebih banyak pekerjaan yang diminta daripada yang diizinkan oleh maxjobs.
lhunath
1
Perhatikan juga bahwa ini mengasumsikan skrip Anda tidak melakukan hal lain apa pun yang berkaitan dengan pekerjaan; jika ya, itu akan dihitung sebagai maxjobs juga.
lhunath
1
Anda mungkin ingin menggunakan "jobs -pr" untuk membatasi pekerjaan yang sedang berjalan.
amphetamachine
1
Menambahkan perintah tidur untuk mencegah while loop berulang tanpa jeda, sementara menunggu perintah do-something selesai dijalankan. Jika tidak, loop ini pada dasarnya akan mengambil salah satu inti CPU. Ini juga menjawab kekhawatiran @lhunath.
euphoria83
12

Berikut solusi alternatif yang dapat dimasukkan ke dalam .bashrc dan digunakan untuk satu liner sehari-hari:

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

Untuk menggunakannya, yang harus dilakukan adalah meletakkan &setelah pekerjaan dan panggilan pwait, parameter memberikan jumlah proses paralel:

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

Ini akan lebih baik untuk digunakan waitdaripada sibuk menunggu hasil jobs -p, tetapi tampaknya tidak ada solusi yang jelas untuk menunggu sampai salah satu pekerjaan yang diberikan selesai daripada semuanya.

Grumbel
sumber
11

Alih-alih bash biasa, gunakan Makefile, lalu tentukan jumlah tugas simultan dengan make -jXX adalah jumlah tugas yang akan dijalankan sekaligus.

Atau Anda dapat menggunakan wait(" man wait"): meluncurkan beberapa proses anak, panggil wait- ini akan keluar saat proses anak selesai.

maxjobs = 10

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

job ( ){
...
}

Jika Anda perlu menyimpan hasil pekerjaan, maka tetapkan hasilnya ke variabel. Setelah waitAnda baru memeriksa apa isi variabel.

skolima
sumber
1
Terima kasih untuk ini, meskipun kodenya belum selesai, itu memberi saya jawaban untuk masalah yang saya alami di tempat kerja.
gerikson
satu-satunya masalah adalah jika Anda mematikan skrip latar depan (yang memiliki loop), pekerjaan yang sedang berjalan tidak akan terbunuh bersama
Girardi
8

Mungkin mencoba utilitas paralel daripada menulis ulang loop? Saya penggemar berat xjobs. Saya menggunakan xjobs sepanjang waktu untuk menyalin file secara massal di seluruh jaringan kami, biasanya saat menyiapkan server database baru. http://www.maier-komor.de/xjobs.html

tessein
sumber
7

Jika Anda terbiasa dengan makeperintah tersebut, sering kali Anda dapat mengekspresikan daftar perintah yang ingin Anda jalankan sebagai makefile. Misalnya, jika Anda perlu menjalankan $ SOME_COMMAND pada file * .input yang masing-masing menghasilkan * .output, Anda dapat menggunakan makefile

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

% .output:% .input
    $ (SOME_COMMAND) $ <$ @

semua: $ (OUTPUT)

dan kemudian lari saja

make -j <NUMBER>

untuk menjalankan maksimal NUMBER perintah secara paralel.

Idelic
sumber
6

Meskipun melakukan ini dengan benar bashmungkin tidak mungkin, Anda dapat melakukan semi-kanan dengan cukup mudah. bstarkmemberikan perkiraan yang adil tentang hak tetapi miliknya memiliki kekurangan berikut:

  • Pemisahan kata: Anda tidak dapat meneruskan pekerjaan apa pun yang menggunakan salah satu karakter berikut dalam argumennya: spasi, tab, baris baru, bintang, tanda tanya. Jika Anda melakukannya, banyak hal akan rusak, mungkin tidak terduga.
  • Ini bergantung pada sisa skrip Anda untuk tidak melatarbelakangi apa pun. Jika Anda melakukannya, atau nanti Anda menambahkan sesuatu ke skrip yang dikirim di latar belakang karena Anda lupa bahwa Anda tidak diizinkan untuk menggunakan pekerjaan latar belakang karena cuplikannya, semuanya akan rusak.

Perkiraan lain yang tidak memiliki kekurangan ini adalah sebagai berikut:

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

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

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

    wait "${pids[@]}"
}

Perhatikan bahwa yang satu ini mudah beradaptasi untuk juga memeriksa kode keluar dari setiap pekerjaan saat berakhir sehingga Anda dapat memperingatkan pengguna jika pekerjaan gagal atau menetapkan kode keluar scheduleAllsesuai dengan jumlah pekerjaan yang gagal, atau sesuatu.

Masalah dengan kode ini hanya itu:

  • Ini menjadwalkan empat (dalam hal ini) pekerjaan pada satu waktu dan kemudian menunggu keempatnya selesai. Beberapa mungkin selesai lebih cepat daripada yang lain yang akan menyebabkan batch berikutnya dari empat pekerjaan menunggu hingga batch sebelumnya selesai paling lama.

Solusi yang menangani masalah terakhir ini harus digunakan kill -0untuk melakukan polling apakah ada proses yang hilang, bukan waitdan menjadwalkan pekerjaan berikutnya. Namun, itu menimbulkan masalah kecil baru: Anda memiliki kondisi balapan antara pekerjaan berakhir, dan kill -0memeriksa apakah pekerjaan sudah berakhir. Jika pekerjaan berakhir dan proses lain di sistem Anda dimulai pada saat yang sama, mengambil PID acak yang kebetulan merupakan pekerjaan yang baru saja selesai, kill -0tidak akan melihat pekerjaan Anda telah selesai dan segala sesuatunya akan rusak lagi.

Solusi sempurna tidak mungkin dilakukan bash.

lhunath
sumber
3

fungsi untuk pesta:

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
}

menggunakan:

cat my_commands | parallel -j 4
ilnar
sumber
Penggunaannya make -jpintar, tetapi tanpa penjelasan dan gumpalan kode Awk hanya-tulis itu, saya menahan diri dari upvoting.
tripleee
2

Proyek yang saya kerjakan menggunakan perintah tunggu untuk mengontrol proses paralel shell (sebenarnya ksh). Untuk mengatasi kekhawatiran Anda tentang IO, pada OS modern, eksekusi paralel mungkin benar-benar akan meningkatkan efisiensi. Jika semua proses membaca blok yang sama pada disk, hanya proses pertama yang harus menggunakan perangkat keras fisik. Proses lain sering kali dapat mengambil blok dari cache disk OS di memori. Jelas, membaca dari memori beberapa kali lipat lebih cepat daripada membaca dari disk. Selain itu, manfaatnya tidak memerlukan perubahan pengkodean.

Jon Ericson
sumber
1

Ini mungkin cukup baik untuk sebagian besar tujuan, tetapi tidak optimal.

#!/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
kucing
sumber
1

Inilah cara saya menyelesaikan masalah ini dalam skrip 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
Fernando
sumber
1

Terlambat banget ke pesta di sini, tapi ini solusi lain.

Banyak solusi yang tidak menangani spasi / karakter khusus dalam perintah, tidak menjalankan tugas N setiap saat, memakan cpu dalam loop sibuk, atau bergantung pada dependensi eksternal (misalnya GNU parallel).

Dengan inspirasi untuk penanganan proses mati / zombie , inilah solusi bash murni:

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
}

Dan penggunaan sampel:

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[@]}"

Hasil:

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

Untuk penanganan keluaran per proses $$bisa digunakan untuk login ke suatu file, contoh:

function job_done {
    cat "$1.log"
}

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

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

Keluaran:

1 56871
2 56872
Skrat
sumber
0

Anda dapat menggunakan loop bersarang sederhana (gantikan bilangan bulat yang sesuai untuk N dan M di bawah):

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

Ini akan mengeksekusi do_something N * M kali dalam M putaran, setiap putaran mengeksekusi N pekerjaan secara paralel. Anda dapat membuat N sama dengan jumlah CPU yang Anda miliki.

Adam Zalcman
sumber
0

Solusi saya untuk selalu menjaga sejumlah proses berjalan, terus melacak kesalahan dan menangani proses ubnterruptible / zombie:

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
}

Pemakaian:

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"
Orsiris de Jong
sumber
-1

$ DOMAINS = "daftar beberapa domain dalam perintah" untuk foo in some-command do

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

selesai

Ndomain =echo $DOMAINS |wc -w

untuk saya di $ (seq 1 1 $ Ndomains) lakukan echo "tunggu $ {pekerjaan [$ i]}" tunggu "$ {pekerjaan [$ i]}" selesai

dalam konsep ini akan bekerja untuk memparalelkan. Yang terpenting adalah baris terakhir eval adalah '&' yang akan menempatkan perintah ke latar belakang.

Mendongkrak
sumber