Paralléliser script Bash avec le nombre maximum de processus
-
09-06-2019 - |
Question
Disons que j'ai une boucle en Bash:
for foo in `some-command`
do
do-something $foo
done
do-something
est en cpu, et j'ai un beau brillant à 4 cœurs du processeur.J'aimerais être capable d'exécuter jusqu'à 4 do-something
's à la fois.
L'approche naïve semble être:
for foo in `some-command`
do
do-something $foo &
done
Cela va lancer tous do-something
s à la fois, mais il y a quelques inconvénients, surtout que quelque chose peut aussi avoir une importante I/O duquel l'exécution de la tous à la fois pourrait ralentir un peu.L'autre problème, c'est que ce bloc de code renvoie immédiatement, donc pas moyen de faire d'autres travaux lorsque tous les do-something
s sont finis.
Comment écririez-vous de cette boucle, il y a donc toujours X do-something
s running à la fois?
La solution
En fonction de ce que vous voulez faire xargs peut aussi aider (ici:la conversion de documents avec pdf2ps):
cpus=$( ls -d /sys/devices/system/cpu/cpu[[:digit:]]* | wc -w )
find . -name \*.pdf | xargs --max-args=1 --max-procs=$cpus pdf2ps
À partir de la documentation:
--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.
Autres conseils
Avec GNU Parallèle http://www.gnu.org/software/parallel/ vous pouvez écrire:
some-command | parallel do-something
GNU Parallèle prend également en charge l'exécution de travaux sur des ordinateurs distants.Ce sera exécuté par un PROCESSEUR de base sur les ordinateurs distants, même si elles peuvent avoir un nombre différent de noyaux:
some-command | parallel -S server1,server2 do-something
Un exemple plus complexe:Ici, nous avons la liste des fichiers que nous voulons my_script pour exécuter sur.Les fichiers ont l'extension (peut-être .jpeg).Nous voulons que la sortie de my_script être mis en regard des fichiers dans basename.(p. ex.foo.jpeg -> foo.out).Nous voulons exécuter my_script une fois pour chaque cœur de l'ordinateur a et nous voulons exécuter sur l'ordinateur local, trop.Pour les ordinateurs à distance nous voulons que le dossier soit traité transférées à l'ordinateur.Lorsque my_script finitions, nous voulons foo.hors transférés ensuite, nous voulons foo.jpeg et toto.hors supprimé de l'ordinateur distant:
cat list_of_files | \
parallel --trc {.}.out -S server1,server2,: \
"my_script {} > {.}.out"
GNU Parallèle permet de s'assurer de la sortie de chaque tâche n'est pas de mélanger, de sorte que vous pouvez utiliser la sortie comme entrée pour un autre programme:
some-command | parallel do-something | postprocess
Voir les vidéos pour plus d'exemples: 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 ...
Au lieu d'une plaine bash, utiliser un Makefile, puis spécifiez le nombre de simultanée des emplois avec make -jX
où X est le nombre de travaux à exécuter à la fois.
Ou vous pouvez utiliser wait
("man wait
"):lancer plusieurs processus fils, appel wait
- elle va sortir quand l'enfant les processus de finition.
maxjobs = 10
foreach line in `cat file.txt` {
jobsrunning = 0
while jobsrunning < maxjobs {
do job &
jobsrunning += 1
}
wait
}
job ( ){
...
}
Si vous avez besoin de stocker l'emploi du résultat, puis d'affecter le résultat à une variable.Après wait
vous venez de vérifier que la variable contient.
Voici une solution alternative qui peut être inséré dans .bashrc et utilisés pour tous les jours un liner:
function pwait() {
while [ $(jobs -p | wc -l) -ge $1 ]; do
sleep 1
done
}
Pour l'utiliser, tout ce qu'on a à faire est de mettre &
après les travaux et d'un pwait appel, le paramètre donne le nombre de processus parallèles:
for i in *; do
do_something $i &
pwait 10
done
Il serait plus agréable à utiliser wait
au lieu de occupé attente sur la sortie de jobs -p
, mais il ne semble pas être une solution évidente à attendre jusqu'à une de ces emplois est fini au lieu d'un tous.
Peut-être essayer un parallélisation d'utilité au lieu de réécrire la boucle?Je suis un grand fan de xjobs.J'utilise xjobs tout le temps à la messe de copier des fichiers à travers notre réseau, généralement lors de la configuration d'un nouveau serveur de base de données.http://www.maier-komor.de/xjobs.html
Tout en faisant ce droit dans bash
est probablement impossible, vous pouvez faire une demi-droite assez facilement. bstark
a donné une juste approximation de droit, mais le son a la suite de défauts:
- Couper un mot:Vous ne pouvez pas passer toutes les emplois que d'utiliser les caractères suivants dans leurs arguments:des espaces, des tabulations, retours à la ligne, des étoiles, des points d'interrogation.Si vous le faites, les choses vont casser, éventuellement de façon inattendue.
- Il s'appuie sur le reste de votre script à pas de fond quoi que ce soit.Si vous le faites, ou plus tard, vous ajouter quelque chose pour le script qui est envoyé par le fond, car vous avez oublié que vous n'étiez pas autorisé à utiliser backgrounded emploi en raison de son extrait, les choses vont se briser.
Un autre rapprochement qui n'a pas ces défauts est la suivante:
scheduleAll() {
local job i=0 max=4 pids=()
for job; do
(( ++i % max == 0 )) && {
wait "${pids[@]}"
pids=()
}
bash -c "$job" & pids+=("$!")
done
wait "${pids[@]}"
}
Notez que celui-ci est facilement adaptable à vérifier aussi le code de sortie de chaque travail que ça se termine ainsi, vous pouvez alerter l'utilisateur si un travail d'échec ou de définir un code de sortie pour scheduleAll
selon le nombre d'emplois qui a échoué, ou quelque chose.
Le problème avec ce code, c'est juste que:
- L'horaire de quatre (dans ce cas) les emplois à un moment et puis attend tous les quatre à la fin.Certains sont peut-être fait plus tôt que d'autres, qui sera la cause de la prochaine fournée de quatre emplois d'attendre jusqu'à ce que le plus long du lot précédent est terminé.
Une solution qui prend en charge ce dernier devra utiliser kill -0
de scrutin si l'un de ces procédés ont disparu à la place de la wait
et le calendrier de travail suivant.Cependant, qui introduit un nouveau problème:vous avez une condition de concurrence entre un travail de fin, et la kill -0
vérifier si il est terminé.Si la fin de l'emploi et un autre processus sur votre système démarre en même temps, en prenant un hasard PID qui se trouve être celui de l'emploi qui vient de terminer, la kill -0
ne pas l'avis de votre travail d'avoir fini, et les choses vont briser à nouveau.
Une solution parfaite n'est pas possible dans bash
.
Si vous êtes familier avec le make
de commande, la plupart du temps, vous pouvez exprimer la liste des commandes que vous souhaitez exécuter comme un makefile.Par exemple, si vous avez besoin d'exécuter $SOME_COMMAND sur les fichiers *.d'entrée dont chacune produit *.de sortie, vous pouvez utiliser le makefile
INPUT = a.input b.input OUTPUT = $(INPUT:.input=.output) %.output : %.input $(SOME_COMMAND) $< $@ all: $(OUTPUT)
et puis il suffit d'exécuter
make -j<NUMBER>
pour exécuter au plus grand NOMBRE des commandes en parallèle.
fonction pour 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
}
à l'aide de:
cat my_commands | parallel -j 4
Le projet que je travaille sur les utilisations de l' attendre commande de contrôle parallèle shell (ksh en fait) des processus.Pour répondre à vos préoccupations au sujet de IO, sur un système d'exploitation moderne, il est possible de l'exécution en parallèle sera effectivement augmenter l'efficacité.Si tous les processus de la lecture des mêmes blocs sur le disque, seul le premier aura pour frapper le matériel physique.Les autres processus seront souvent en mesure de récupérer le bloc à partir de l'OS du disque de cache en mémoire.Évidemment, la lecture de la mémoire est de plusieurs ordres de grandeur plus rapide que la lecture à partir du disque.Aussi, l'avantage ne nécessite aucun codage des changements.
Ce pourrait être assez bon pour la plupart des besoins, mais n'est pas optimale.
#!/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
Vraiment en retard à la fête, voici une autre solution.
Beaucoup de solutions ne gère pas les espaces et les caractères spéciaux dans les commandes, ne le gardez pas N travaux en cours d'exécution à tout moment, manger de la cpu dans les boucles, ou de s'appuyer sur les dépendances externes (par ex.GNU parallel
).
Avec source d'inspiration pour les morts/zombie la gestion de processus, voici un pur bash solution:
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
}
Et exemple d'utilisation:
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[@]}"
La sortie:
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
Pour chaque processus de sortie de la manipulation $$
pourrait être utilisé pour se connecter à un fichier, par exemple:
function job_done {
cat "$1.log"
}
cmds=( \
"echo 1 \$\$ >\$\$.log" \
"echo 2 \$\$ >\$\$.log" \
)
run_parallel_jobs 2 "job_done" "${cmds[@]}"
Sortie:
1 56871
2 56872
Vous pouvez utiliser un simple imbriqués pour la boucle (substitut approprié des entiers N et M ci-dessous):
for i in {1..N}; do
(for j in {1..M}; do do_something; done & );
done
Cela permettra d'exécuter faire_quelque_chose N*M temps dans la M des tours, chaque tour de la N de l'exécution de travaux en parallèle.Vous pouvez faire N égal au nombre de Processeurs que vous avez.
Voici comment j'ai réussi à résoudre ce problème dans un 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
Ma solution pour toujours garder un certain nombre de processus en cours d'exécution, garder le suivi des erreurs et les traiter ubnterruptible / zombie processus:
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
}
Utilisation:
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"
$DOMAINES = "liste de quelques-uns de domaine dans les commandes"
pour les foo dans some-command
faire
eval `some-command for $DOMAINS` &
job[$i]=$!
i=$(( i + 1))
fait
Ndomains=echo $DOMAINS |wc -w
for i in $(seq 1 1 $Ndomains) faire echo "attendez ${travail[$i]}" wait "${travail[$i]}" fait
dans ce concept de travail pour la parallélisation.important, c'est la dernière ligne de la fonction eval est '&' qui va mettre les commandes à l'arrière.