Paralleliseer Bash-script met maximaal aantal processen

Laten we zeggen dat ik een lus in Bash heb:

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

do-somethingis cpu-gebonden en ik heb een mooie glanzende 4-coreprocessor. Ik wil graag tot 4 do-something‘s tegelijk kunnen uitvoeren.

De naïeve benadering lijkt te zijn:

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

Hiermee worden alledo-somethings tegelijk uitgevoerd, maar er zijn een paar nadelen, voornamelijk dat doe-iets ook een aantal significante I/O kan hebben die allemaaltegelijk kan wat langzamer gaan. Het andere probleem is dat dit codeblok onmiddellijk terugkeert, dus er is geen manier om ander werk te doen als alle do-something‘s klaar zijn.

Hoe zou je deze lus schrijven, zodat er altijd X do-something‘s tegelijk worden uitgevoerd?


Antwoord 1, autoriteit 100%

Afhankelijk van wat u wilt doen, kan xargs ook helpen (hier: documenten converteren met pdf2ps):

cpus=$( ls -d /sys/devices/system/cpu/cpu[[:digit:]]* | wc -w )
find . -name \*.pdf | xargs --max-args=1 --max-procs=$cpus  pdf2ps

Uit de documenten:

--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.

Antwoord 2, autoriteit 59%

Met GNU Parallel http://www.gnu.org/software/parallel/je kunt schrijven:

some-command | parallel do-something

GNU Parallel ondersteunt ook het uitvoeren van taken op externe computers. Dit zal één per CPU-kern uitvoeren op de externe computers – zelfs als ze een verschillend aantal kernen hebben:

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

Een meer geavanceerd voorbeeld: hier geven we een lijst met bestanden waarvan we willen dat my_script wordt uitgevoerd. Bestanden hebben de extensie (misschien .jpeg). We willen dat de uitvoer van my_script naast de bestanden in basename.out wordt geplaatst (bijv. foo.jpeg -> foo.out). We willen my_script één keer uitvoeren voor elke kern die de computer heeft en we willen het ook op de lokale computer uitvoeren. Voor de externe computers willen we dat het bestand dat wordt verwerkt naar de opgegeven computer wordt overgebracht. Als my_script klaar is, willen we dat foo.out terug wordt overgedragen en dat foo.jpeg en foo.out worden verwijderd van de externe computer:

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

GNU Parallel zorgt ervoor dat de uitvoer van elke taak niet gemengd is, dus u kunt de uitvoer gebruiken als invoer voor een ander programma:

some-command | parallel do-something | postprocess

Bekijk de video’s voor meer voorbeelden: https://www.youtube.com/playlist?list =PL284C9FF2488BC6D1


Antwoord 3, autoriteit 32%

maxjobs=4
parallelliseren () {
    terwijl [ $# -gt 0]; doen
        jobcnt=(`banen -p`)
        if [ ${#jobcnt[@]} -lt $maxjobs]; dan
            iets doen $1 &
            verschuiving
        anders
            slapen 1
        fi
    gedaan
    wachten
}
parallel arg1 arg2 "5 args naar derde taak" arg4 ...

Antwoord 4, autoriteit 25%

Hier een alternatieve oplossing die in .bashrc kan worden ingevoegd en voor alledaagse one-liners kan worden gebruikt:

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

Om het te gebruiken, hoef je alleen maar &na de jobs en een pwait-aanroep te plaatsen, de parameter geeft het aantal parallelle processen aan:

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

Het zou leuker zijn om waitte gebruiken in plaats van bezig te wachten op de output van jobs -p, maar er lijkt geen voor de hand liggende oplossing te zijn om te wachten tot een van de opgegeven taken is voltooid in plaats van ze allemaal.


Antwoord 5, autoriteit 16%

Gebruik in plaats van een gewone bash een Makefile en specificeer vervolgens het aantal gelijktijdige taken met make -jXwaarbij X het aantal taken is dat tegelijk moet worden uitgevoerd.

Of u kunt wait(“man wait“) gebruiken: start verschillende onderliggende processen, bel wait– het wordt afgesloten wanneer het kind processen eindigen.

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

Als u het resultaat van de taak moet opslaan, wijs het resultaat dan toe aan een variabele. Na waitcontroleer je gewoon wat de variabele bevat.


Antwoord 6, autoriteit 12%

Misschien een parallellisatieprogramma proberen in plaats van de lus te herschrijven? Ik ben een grote fan van xjobs. Ik gebruik de hele tijd xjobs om massaal bestanden over ons netwerk te kopiëren, meestal bij het opzetten van een nieuwe databaseserver.
http://www.maier-komor.de/xjobs.html


Antwoord 7, autoriteit 10%

Als je bekend bent met het makecommando, kun je de lijst met commando’s die je wilt uitvoeren meestal als een makefile uitdrukken. Als u bijvoorbeeld $SOME_COMMAND moet uitvoeren op bestanden *.input die elk *.output produceren, kunt u de makefile

gebruiken

INPUT = a.input b.input
UITGANG = $(INPUT:.input=.output)
%.uitvoer invoer
  $(SOME_COMMAND) $< $@
alles: $(OUTPUT)

en dan gewoon rennen

make -j<NUMBER>

om maximaal NUMBER opdrachten parallel uit te voeren.


Antwoord 8, autoriteit 9%

Hoewel het waarschijnlijk onmogelijk is om dit goed te doen in bash, kun je redelijk eenvoudig een semi-rechts doen. bstarkgaf een redelijke benadering van het recht, maar hij heeft de volgende gebreken:

  • Woorden splitsen: u kunt er geen taken aan doorgeven die een van de volgende tekens in hun argumenten gebruiken: spaties, tabs, nieuwe regels, sterren, vraagtekens. Als je dat doet, gaan dingen kapot, mogelijk onverwachts.
  • Het is afhankelijk van de rest van je script om niets op de achtergrond te zetten. Als je dat doet, of als je later iets toevoegt aan het script dat op de achtergrond wordt verzonden omdat je bent vergeten dat je geen achtergrondtaken mag gebruiken vanwege zijn fragment, gaan de dingen kapot.

Een andere benadering die deze gebreken niet heeft, is de volgende:

scheduleAll() {
    local job i=0 max=4 pids=()
    for job; do
        (( ++i % max == 0 )) && {
            wait "${pids[@]}"
            pids=()
        }
        bash -c "$job" & pids+=("$!")
    done
    wait "${pids[@]}"
}

Merk op dat deze eenvoudig kan worden aangepast om ook de afsluitcode van elke taak te controleren wanneer deze eindigt, zodat u de gebruiker kunt waarschuwen als een taak mislukt of een afsluitcode kunt instellen voor scheduleAllop basis van het aantal van banen die zijn mislukt, of zoiets.

Het probleem met deze code is precies dat:

  • Het plant vier (in dit geval) taken tegelijk en wacht dan tot alle vier zijn afgelopen. Sommige zijn misschien eerder klaar dan andere, waardoor de volgende batch van vier taken moet wachten tot de langste van de vorige batch is voltooid.

Een oplossing die dit laatste probleem oplost, zou kill -0moeten gebruiken om te peilen of een van de processen is verdwenen in plaats van waiten de volgende te plannen functie. Dat introduceert echter een klein nieuw probleem: je hebt een race-conditie tussen het beëindigen van een taak en de kill -0die controleert of deze is beëindigd. Als de taak is beëindigd en tegelijkertijd een ander proces op uw systeem opstart, waarbij een willekeurige PID wordt genomen die toevallig die van de zojuist voltooide taak is, zal de kill -0uw taak niet opmerken als het klaar is, gaan de dingen weer kapot.

Een perfecte oplossing is niet mogelijk in bash.


Antwoord 9, autoriteit 4%

functie voor 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
}

gebruik:

cat my_commands | parallel -j 4

Antwoord 10, autoriteit 3%

Het project waar ik aan werk gebruikt het waitcommando om parallelle shell (ksh eigenlijk) processen te besturen. Om uw zorgen over IO weg te nemen, op een modern besturingssysteem, is het mogelijk dat parallelle uitvoering de efficiëntie daadwerkelijk zal verhogen. Als alle processen dezelfde blokken op schijf lezen, hoeft alleen het eerste proces de fysieke hardware te raken. De andere processen kunnen het blok vaak uit de schijfcache van het besturingssysteem in het geheugen halen. Het is duidelijk dat het lezen uit het geheugen een aantal orden van grootte sneller is dan het lezen van een schijf. Het voordeel vereist ook geen coderingswijzigingen.


Antwoord 11, autoriteit 3%

Echtlaat op het feest hier, maar hier is een andere oplossing.

Veel oplossingen verwerken geen spaties/speciale tekens in de commando’s, houden N jobs niet altijd actief, eten cpu in drukke lussen of vertrouwen op externe afhankelijkheden (bijv. GNU parallel).

Met inspiratie voor de verwerking van dead/zombie-processenis hier een pure bash-oplossing:

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
}

En voorbeeldgebruik:

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

De uitvoer:

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

Voor uitvoerverwerking per proces kan $$worden gebruikt om in te loggen op een bestand, bijvoorbeeld:

function job_done {
    cat "$1.log"
}
cmds=( \
    "echo 1 \$\$ >\$\$.log" \
    "echo 2 \$\$ >\$\$.log" \
)
run_parallel_jobs 2 "job_done" "${cmds[@]}"

Uitvoer:

1 56871
2 56872

Antwoord 12

$DOMAINS = “lijst van een aantal domeinen in commando’s”
voor foo in some-command
doen

eval `some-command for $DOMAINS` &
    job[$i]=$!
    i=$(( i + 1))

klaar

Ndomains=echo $DOMAINS |wc -w

voor i in $(seq 1 1 $Ndomains)
doen
echo “wacht op ${job[$i]}”
wacht “${job[$i]}”
klaar

in dit concept zal werken voor de parallellisatie. het belangrijkste is dat de laatste regel van de evaluatie ‘&’ is
die de commando’s op de achtergrond plaatst.

Other episodes