Bash komut dosyasını maksimum işlem sayısıyla paralel hale getirin


87

Bash'de bir döngüm olduğunu varsayalım:

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

do-somethingcpu bağlı ve güzel, parlak bir 4 çekirdekli işlemcim var. do-somethingAynı anda 4'e kadar koşabilmek istiyorum .

Saf yaklaşım şöyle görünüyor:

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

Bu çalışacak tüm do-something kerede s, ama esas birkaç olumsuzlukları vardır do-bir şey de gerçekleştirerek bazı önemli I / O'yu olabilir hepsi biraz yavaşlatabilir seferde. Diğer sorun, bu kod bloğunun hemen dönmesidir, bu nedenle tüm kodlar do-somethingbittiğinde başka bir iş yapmanın yolu yoktur .

Her zaman do-somethingaynı anda çalışan X'ler olması için bu döngüyü nasıl yazarsınız ?


2
Bir yan düğüm olarak, ilkel için bash'a make's -j seçeneğini eklemeyi hayal ettim. Her zaman işe yaramazdı, ancak döngünün gövdesinin her yineleme için benzersiz bir şey yapacağını bildiğiniz bazı basit durumlarda, sadece "-j 4 ..." demek oldukça temiz olurdu.
gevşeyin

1
Performans sorunlarını azaltan ve ayrı tutulan alt işlem gruplarına izin veren bir bash çözümü için stackoverflow.com/questions/1537956/… adresine çapraz referans .
paxdiablo

Yanıtlar:


63

Ne yapmak istediğinize bağlı olarak xargs da yardımcı olabilir (burada: belgeleri pdf2ps ile dönüştürme):

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

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

Dokümanlardan:

--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
Bu yöntem bence en şık çözüm. Paranoyak beri, hep böyle kullanmak, hariç find [...] -print0ve xargs -0.
amphetamachine

7
cpus=$(getconf _NPROCESSORS_ONLN)
mr.spuratic

1
Kılavuzdan, neden --max-procs=0mümkün olduğunca çok işlem elde etmek için kullanmayasınız ?
EverythingRightPlace

@EverythingRightPlace, soru açıkça mevcut işlemcilerden daha fazla işlem istemiyor. --max-procs=0daha çok sorucunun girişimi gibidir (argüman kadar çok işlemi başlatın).
Toby Speight

39

GNU Parallel http://www.gnu.org/software/parallel/ ile şunları yazabilirsiniz:

some-command | parallel do-something

GNU Parallel ayrıca uzak bilgisayarlarda çalışan işleri destekler. Bu, uzak bilgisayarlarda farklı sayıda çekirdeğe sahip olsalar bile CPU çekirdeği başına bir tane çalıştıracaktır:

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

Daha gelişmiş bir örnek: Burada, my_script'in çalışmasını istediğimiz dosyaların bir listesi var. Dosyaların uzantısı vardır (belki .jpeg). My_script'in çıktısının basename.out'taki dosyaların yanına yerleştirilmesini istiyoruz (örneğin foo.jpeg -> foo.out). My_script'i bilgisayarın sahip olduğu her çekirdek için bir kez çalıştırmak istiyoruz ve onu yerel bilgisayarda da çalıştırmak istiyoruz. Uzak bilgisayarlar için dosyanın işlenmesini ve verilen bilgisayara aktarılmasını istiyoruz. My_script bittiğinde, foo.out'un geri aktarılmasını istiyoruz ve ardından foo.jpeg ve foo.out'un uzak bilgisayardan kaldırılmasını istiyoruz:

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

GNU Parallel, her bir işin çıktısının karışmamasını sağlar, böylece çıktıyı başka bir program için girdi olarak kullanabilirsiniz:

some-command | parallel do-something | postprocess

Daha fazla örnek için videolara bakın: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1


1
findBir dosya listesi oluşturmak için bir komut kullanırken bunun gerçekten yararlı olduğunu unutmayın , çünkü yalnızca bir dosya adı içinde bir boşluk oluştuğunda sorunu önlemekle for i in ...; dokalmaz, aynı zamanda find, find -name \*.extension1 -or -name \*.extension2GNU paralellerinin {.} Çok iyi işleyebileceğini de yapabilir.
Leo Izen

Artı 1 catelbette faydasız
üçlü

@tripleee Re: Kedinin gereksiz kullanımı. Bakınız oletange.blogspot.dk/2013/10/useless-use-of-cat.html
Ole Tange

Oh, sensin! Bu arada, o blogdaki bağlantıyı güncelleyebilir misin? Partmaps.org konumu ne yazık ki öldü, ancak İki yeniden yönlendirici çalışmaya devam etmeli.
üçlü

22
maxjobs = 4
parallelize () {
        süre [$ # -gt 0]; yapmak
                jobcnt = (`işler -p`)
                eğer [$ {# jobcnt [@]} -lt $ maxjobs]; sonra
                        bir şeyler yap ve
                        vardiya  
                Başka
                        uyku 1
                fi
        bitti
        Bekle
}

arg1 arg2 "5 değiştirgeyi üçüncü işe paralelleştir" arg4 ...

10
Tartışmalarda boşluk gerektiren işler kötü bir şekilde başarısız olacak şekilde burada bazı ciddi eksik alıntılar olduğunu anlayın; dahası, bu komut dosyası, maxjobs'un izin verdiğinden daha fazla iş talep edilirse bazı işlerin bitirilmesini beklerken CPU'nuzu canlı canlı yiyecektir.
lhunath

1
Ayrıca bu, betiğinizin işlerle ilgili başka hiçbir şey yapmadığını varsayar; eğer öyleyseniz, bunları da maxjobs'a sayar.
lhunath

1
Çalışan işleri sınırlandırmak için "jobs -pr" kullanmak isteyebilirsiniz.
amphetamachine

1
While döngüsünün herhangi bir kesinti olmadan tekrarlamasını önlemek için bir uyku komutu eklendi ve zaten çalışan bir şeyler yap komutlarının bitmesini bekler. Aksi takdirde, bu döngü esas olarak CPU çekirdeklerinden birini alırdı. Bu aynı zamanda @lhunath'ın endişesini de giderir.
euphoria83

12

İşte .bashrc içine eklenebilen ve her gün tek astar için kullanılabilen alternatif bir çözüm:

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

Kullanmak için tek yapması gereken &işlerin ve bir pwait çağrısının arkasına koymaktır, parametre paralel işlemlerin sayısını verir:

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

waitÇıktısını beklemekle meşgul olmak yerine kullanmak daha güzel olurdu jobs -p, ancak verilen işlerin hepsi yerine bitene kadar beklemek için bariz bir çözüm yok gibi görünüyor.


11

Düz bir bash yerine, bir Makefile kullanın, ardından make -jXX'in aynı anda çalıştırılacak iş sayısı olduğu eşzamanlı işlerin sayısını belirtin .

Ya da wait(" man wait") kullanabilirsiniz : birkaç çocuk süreci başlatın, arayın wait- çocuk süreçler bittiğinde bu işlemden çıkar.

maxjobs = 10

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

job ( ){
...
}

İşin sonucunu kaydetmeniz gerekiyorsa, sonucunu bir değişkene atayın. waitDeğişkenin ne içerdiğini kontrol ettikten sonra .


1
Bunun için teşekkürler, kod bitmemiş olmasına rağmen bana işte yaşadığım bir sorunun cevabını veriyor.
gerikson

tek sorun, ön plandaki komut dosyasını (döngülü olanı) öldürürseniz, çalışan işler birlikte öldürülmeyecek
Girardi

8

Döngüyü yeniden yazmak yerine bir paralelleştirme yardımcı programını deneyebilir misiniz? Xjobs hayranıyım. Xjobs’u, ​​genellikle yeni bir veritabanı sunucusu kurarken, ağımızdaki dosyaları toplu kopyalamak için her zaman kullanıyorum. http://www.maier-komor.de/xjobs.html


7

makeKomuta aşina iseniz , çoğu zaman bir makefile olarak çalıştırmak istediğiniz komutların listesini ifade edebilirsiniz. Örneğin, her biri * .output üreten * .input dosyalarında $ SOME_COMMAND çalıştırmanız gerekiyorsa makefile dosyasını kullanabilirsiniz.

INPUT = a.input b.input
ÇIKIŞ = $ (GİRİŞ: .input = .output)

%.çıkış giriş
    $ (SOME_COMMAND) $ <$ @

tümü: $ (OUTPUT)

ve sonra sadece koş

make -j <NUMBER>

paralel olarak en fazla NUMBER komutu çalıştırmak için.


6

Bunu doğru yapmak bashmuhtemelen imkansız olsa da, yarı-doğruyu oldukça kolay bir şekilde yapabilirsiniz. bstarkhakkın adil bir şekilde tahmin edilmesini sağladı ancak aşağıdaki kusurları var:

  • Sözcük bölme: Argümanlarında şu karakterlerden herhangi birini kullanan işleri iletemezsiniz: boşluklar, sekmeler, yeni satırlar, yıldızlar, soru işaretleri. Bunu yaparsanız, muhtemelen beklenmedik bir şekilde işler bozulacaktır.
  • Senaryonuzun geri kalanına hiçbir şeyin arka plan yapmamasına dayanır. Bunu yaparsanız veya daha sonra betiğe arka planda gönderilen bir şey eklerseniz çünkü onun pasajı nedeniyle arka planda çalışan işleri kullanmanıza izin verilmediğini unuttuysanız, işler bozulacaktır.

Bu kusurlara sahip olmayan başka bir yaklaşım şudur:

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

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

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

    wait "${pids[@]}"
}

Bunun, her bir işin çıkış kodunu sona erdiğinde kontrol etmek için kolayca uyarlanabileceğini unutmayın, böylece bir iş başarısız olursa kullanıcıyı uyarabilir veya başarısız scheduleAllolan işlerin miktarına veya başka bir şeye göre bir çıkış kodu ayarlayabilirsiniz .

Bu kodla ilgili sorun şudur:

  • Bir seferde dört (bu durumda) iş planlar ve ardından dördünün de bitmesini bekler. Bazıları diğerlerinden daha erken yapılabilir, bu da bir sonraki dört iş grubunun önceki partinin en uzun olanına kadar beklemesine neden olur.

Bu son sorunla ilgilenen bir çözümün , bir sonraki işi planlamak kill -0yerine süreçlerden herhangi birinin kaybolup kaybolmadığını sorgulamak için kullanılması gerekir wait. Bununla birlikte, bu küçük ve yeni bir sorun ortaya çıkarır: bir işin bitmesi ile kill -0bitip bitmediğini kontrol etmek arasında bir yarış durumunuz var. İş biter ve sisteminizdeki başka bir işlem aynı anda başlarsa, yeni biten işinki olan rastgele bir PID alarak, işinizin bittiğini kill -0fark etmez ve işler tekrar bozulur.

İçinde mükemmel bir çözüm mümkün değildir bash.


3

bash işlevi:

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
}

kullanma:

cat my_commands | parallel -j 4

Kullanımı make -jakıllıca, ancak hiçbir açıklama ve yalnızca yazılabilir Awk kodu bloğu olmadan, oy kullanmaktan kaçınıyorum.
üçlü

2

Üzerinde çalıştığım proje, paralel kabuk (aslında ksh) işlemlerini kontrol etmek için bekleme komutunu kullanıyor . IO ile ilgili endişelerinizi gidermek için, modern bir işletim sisteminde paralel yürütmenin aslında verimliliği artırması mümkündür. Tüm işlemler diskteki aynı blokları okuyorsa, yalnızca ilk işlemin fiziksel donanıma çarpması gerekecektir. Diğer işlemler genellikle bloğu işletim sisteminin bellekteki disk önbelleğinden alabilecektir. Açıktır ki, bellekten okumak, diskten okumaktan birkaç kat daha hızlıdır. Ayrıca, avantaj kodlama değişikliği gerektirmez.


1

Bu, çoğu amaç için yeterince iyi olabilir, ancak optimal değildir.

#!/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

İşte bu sorunu bir bash komut dosyasında çözmeyi nasıl başardım:

 #! /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

Buradaki partiye gerçekten geç, ama işte başka bir çözüm.

Bir çok çözüm, komutlardaki boşlukları / özel karakterleri işlemez, N işi her zaman çalışır durumda tutmaz, yoğun döngülerde cpu yemez veya harici bağımlılıklara (örneğin GNU parallel) güvenir .

İle ölü / zombi işlem gerçekleştirmek üzere ilham , burada saf bash çözüm:

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
}

Ve örnek kullanım:

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

Çıktı:

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

İşlem başına çıktı işleme $$için bir dosyaya oturum açmak için kullanılabilir, örneğin:

function job_done {
    cat "$1.log"
}

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

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

Çıktı:

1 56871
2 56872

0

İç içe geçmiş basit bir for döngüsü kullanabilirsiniz (aşağıdaki N ve M yerine uygun tam sayıları koyun):

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

Bu, M turlarında do_something N * M kez çalıştıracak ve her turda paralel olarak N işi gerçekleştirecektir. N'yi sahip olduğunuz CPU sayısına eşit yapabilirsiniz.


0

Belirli sayıda süreci her zaman çalışır durumda tutmak, hataları takip etmek ve kesintisiz / zombi süreçleri yönetmek için benim çözümüm:

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
}

Kullanım:

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 için komutlarda bazı etki alanlarının listesi"

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

bitti

Ndomains =echo $DOMAINS |wc -w

$ (seq 1 1 $ Ndomain) içindeki i için echo "$ {job [$ i]} için bekle" $ {job [$ i]} "tamamlandı

bu konseptte paralellik için çalışacaktır. önemli olan son değerlendirme satırı komutları arka plana koyacak olan '&' dir.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.