Parallelizza lo script Bash con il numero massimo di processi
-
09-06-2019 - |
Domanda
Diciamo che ho un ciclo in Bash:
for foo in `some-command`
do
do-something $foo
done
do-something
è legato alla CPU e ho un bel processore a 4 core brillante.Mi piacerebbe poter correre fino a 4 do-something
è subito.
L’approccio ingenuo sembra essere:
for foo in `some-command`
do
do-something $foo &
done
Questo funzionerà Tutto do-something
s in una sola volta, ma ci sono un paio di aspetti negativi, principalmente che fare qualcosa può anche avere alcuni I/O significativi che l'esecuzione Tutto subito potrebbe rallentare un po'.L'altro problema è che questo blocco di codice ritorna immediatamente, quindi non c'è modo di fare altro lavoro quando tutto il file do-something
sono finiti.
Come scriveresti questo ciclo in modo che ci siano sempre X do-something
sta correndo subito?
Soluzione
A seconda di cosa vuoi fare, anche xargs può aiutarti (qui:convertire documenti con pdf2ps):
cpus=$( ls -d /sys/devices/system/cpu/cpu[[:digit:]]* | wc -w )
find . -name \*.pdf | xargs --max-args=1 --max-procs=$cpus pdf2ps
Dai documenti:
--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.
Altri suggerimenti
Con GNU Parallel http://www.gnu.org/software/parallel/ tu puoi scrivere:
some-command | parallel do-something
GNU Parallel supporta anche l'esecuzione di lavori su computer remoti.Ne verrà eseguito uno per core CPU sui computer remoti, anche se hanno un numero diverso di core:
some-command | parallel -S server1,server2 do-something
Un esempio più avanzato:Qui elenchiamo i file su cui vogliamo che my_script venga eseguito.I file hanno estensione (forse .jpeg).Vogliamo che l'output di my_script venga inserito accanto ai file in basename.out (ad es.foo.jpeg -> foo.out).Vogliamo eseguire my_script una volta per ogni core del computer e vogliamo eseguirlo anche sul computer locale.Per i computer remoti vogliamo che il file venga elaborato trasferito al computer specificato.Quando my_script termina, vogliamo che foo.out venga ritrasferito e poi vogliamo che foo.jpeg e foo.out vengano rimossi dal computer remoto:
cat list_of_files | \
parallel --trc {.}.out -S server1,server2,: \
"my_script {} > {.}.out"
GNU Parallel si assicura che l'output di ciascun lavoro non si mescoli, quindi puoi utilizzare l'output come input per un altro programma:
some-command | parallel do-something | postprocess
Guarda i video per ulteriori esempi: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1
maxjobs=4 parallelize () { while [ $# -gt 0 ] ; do jobcnt=(`jobs -p`) if [ ${#jobcnt[@]} -lt $maxjobs ] ; then do-something $1 & shift else sleep 1 fi done wait } parallelize arg1 arg2 "5 args to third job" arg4 ...
Invece di una semplice bash, usa un Makefile, quindi specifica il numero di lavori simultanei con make -jX
dove X è il numero di lavori da eseguire contemporaneamente.
Oppure puoi usare wait
("man wait
"):avviare diversi processi figli, chiamare wait
- uscirà al termine dei processi figli.
maxjobs = 10
foreach line in `cat file.txt` {
jobsrunning = 0
while jobsrunning < maxjobs {
do job &
jobsrunning += 1
}
wait
}
job ( ){
...
}
Se devi memorizzare il risultato del lavoro, assegna il risultato a una variabile.Dopo wait
devi solo controllare cosa contiene la variabile.
Ecco una soluzione alternativa che può essere inserita in .bashrc e utilizzata per una riga quotidiana:
function pwait() {
while [ $(jobs -p | wc -l) -ge $1 ]; do
sleep 1
done
}
Per utilizzarlo basta mettere &
dopo i lavori e una chiamata pwait, il parametro fornisce il numero di processi paralleli:
for i in *; do
do_something $i &
pwait 10
done
Sarebbe più bello da usare wait
invece di essere occupato ad aspettare l'output di jobs -p
, ma non sembra esserci una soluzione ovvia: aspettare che venga completato uno qualsiasi dei lavori anziché tutti.
Forse provare un'utilità di parallelizzazione invece di riscrivere il ciclo?Sono un grande fan di xjobs.Utilizzo sempre xjobs per copiare in massa i file sulla nostra rete, di solito quando configuro un nuovo server di database.http://www.maier-komor.de/xjobs.html
Mentre lo faccio proprio dentro bash
è probabilmente impossibile, puoi fare un semi-destro abbastanza facilmente. bstark
ha dato una buona approssimazione del giusto ma ha i seguenti difetti:
- Divisione delle parole:Non puoi passargli alcun lavoro che utilizzi uno dei seguenti caratteri nei loro argomenti:spazi, tabulazioni, ritorni a capo, stelle, punti interrogativi.Se lo fai, le cose si romperanno, forse inaspettatamente.
- Si basa sul resto dello script per non creare alcun background.Se lo fai, o in seguito aggiungi qualcosa allo script che viene inviato in background perché hai dimenticato che non ti era consentito utilizzare lavori in background a causa del suo snippet, le cose si interrompono.
Un'altra approssimazione che non presenta questi difetti è la seguente:
scheduleAll() {
local job i=0 max=4 pids=()
for job; do
(( ++i % max == 0 )) && {
wait "${pids[@]}"
pids=()
}
bash -c "$job" & pids+=("$!")
done
wait "${pids[@]}"
}
Tieni presente che questo è facilmente adattabile per controllare anche il codice di uscita di ciascun lavoro al termine in modo da poter avvisare l'utente se un lavoro fallisce o impostare un codice di uscita per scheduleAll
in base alla quantità di lavori falliti o qualcosa del genere.
Il problema con questo codice è proprio questo:
- Pianifica quattro (in questo caso) lavori alla volta e poi attende che tutti e quattro finiscano.Alcuni potrebbero essere eseguiti prima di altri, il che farà sì che il successivo batch di quattro lavori attenda fino al completamento del batch precedente più lungo.
Sarebbe necessario utilizzare una soluzione che si occupi di quest'ultimo problema kill -0
per verificare se qualcuno dei processi è scomparso invece del wait
e programmare il lavoro successivo.Tuttavia, ciò introduce un piccolo nuovo problema:hai una condizione di competizione tra la fine del lavoro e il kill -0
controllando se è finita.Se il lavoro è terminato e contemporaneamente si avvia un altro processo sul sistema, prendendo un PID casuale che sembra essere quello del lavoro appena terminato, il kill -0
non noterai che il tuo lavoro è finito e le cose si romperanno di nuovo.
Una soluzione perfetta non è possibile in bash
.
Se hai familiarità con make
comando, la maggior parte delle volte puoi esprimere l'elenco dei comandi che desideri eseguire come makefile.Ad esempio, se devi eseguire $SOME_COMMAND sui file *.input, ognuno dei quali produce *.output, puoi utilizzare il makefile
INPUT = a.input b.input OUTPUT = $(INPUT:.input=.output) %.output : %.input $(SOME_COMMAND) $< $@ all: $(OUTPUT)
e poi semplicemente corri
make -j<NUMBER>
per eseguire al massimo NUMBER comandi in parallelo.
funzione per 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
}
utilizzando:
cat my_commands | parallel -j 4
Il progetto su cui lavoro utilizza il file Aspettare comando per controllare i processi della shell parallela (ksh in realtà).Per rispondere alle tue preoccupazioni sull'IO, su un sistema operativo moderno, è possibile che l'esecuzione parallela aumenti effettivamente l'efficienza.Se tutti i processi leggono gli stessi blocchi sul disco, solo il primo processo dovrà colpire l'hardware fisico.Gli altri processi saranno spesso in grado di recuperare il blocco dalla cache del disco del sistema operativo in memoria.Ovviamente, la lettura dalla memoria è diversi ordini di grandezza più veloce della lettura dal disco.Inoltre, il vantaggio non richiede modifiche alla codifica.
Questo potrebbe essere sufficiente per la maggior parte degli scopi, ma non è ottimale.
#!/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
Veramente tardi alla festa qui, ma ecco un'altra soluzione.
Molte soluzioni non gestiscono spazi/caratteri speciali nei comandi, non mantengono N lavori sempre in esecuzione, consumano la CPU in cicli occupati o si affidano a dipendenze esterne (ad es.GNU parallel
).
Con ispirazione per la gestione del processo morti/zombi, ecco una soluzione bash pura:
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
}
E utilizzo del campione:
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[@]}"
Il risultato:
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
Per la gestione dell'output per processo $$
potrebbe essere utilizzato per accedere a un file, ad esempio:
function job_done {
cat "$1.log"
}
cmds=( \
"echo 1 \$\$ >\$\$.log" \
"echo 2 \$\$ >\$\$.log" \
)
run_parallel_jobs 2 "job_done" "${cmds[@]}"
Produzione:
1 56871
2 56872
Puoi utilizzare un semplice ciclo for nidificato (sostituisci N e M con numeri interi appropriati di seguito):
for i in {1..N}; do
(for j in {1..M}; do do_something; done & );
done
Questo eseguirà do_qualcosa N*M volte in M round, ogni round eseguendo N lavori in parallelo.Puoi rendere N uguale al numero di CPU che hai.
Ecco come sono riuscito a risolvere questo problema in uno script 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
La mia soluzione per mantenere sempre in esecuzione un determinato numero di processi, tenere traccia degli errori e gestire processi non interrompibili/zombi:
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
}
Utilizzo:
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"
$ Domains = "Elenco di alcuni domini nei comandi" per foo in some-command
Fare
eval `some-command for $DOMAINS` &
job[$i]=$!
i=$(( i + 1))
Fatto
Ndomini=echo $DOMAINS |wc -w
Per i in $ (seq 1 1 $ ndomains) fai eco "Aspetta $ {job [$ i]}" wait "$ {job [$ i]}" fatto
in questo concetto funzionerà per il parallelismo.La cosa importante è che l'ultima riga di Eval è '&' che metterà i comandi agli sfondi.