Paralléliser le script Bash avec un nombre maximum de processus

86

Disons que j'ai une boucle dans Bash:

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

do-somethingest lié au processeur et j'ai un beau processeur 4 cœurs brillant. J'aimerais pouvoir exécuter jusqu'à 4 do-somethingà la fois.

L'approche naïve semble être:

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

Cela se déroulera tous do-something s à la fois, mais il y a quelques inconvénients, surtout que font-quelque chose peut aussi avoir une certaine E / S important qui effectuant tout à la fois pourrait ralentir un peu. L'autre problème est que ce bloc de code retourne immédiatement, donc aucun moyen de faire d'autres travaux lorsque tous les do-somethings sont terminés.

Comment écririez-vous cette boucle pour qu'il y ait toujours X do-somethings en cours d'exécution à la fois?

thelsdj
la source
2
En tant que sidenode, j'ai rêvé d'ajouter l'option -j de make au bash pour primitive. Cela ne fonctionnerait pas toujours, mais pour certains cas simples où vous savez que le corps de la boucle va faire quelque chose d'unique pour chaque itération, il serait assez clair de dire simplement "pour -j 4 ...".
détendre
1
Référence croisée à stackoverflow.com/questions/1537956/… pour une solution bash qui atténue les problèmes de performances et permet des groupes de sous-processus, séparés.
paxdiablo
1
Je recommanderais ma solution stackoverflow.com/a/28965927/340581
Tuttle

Réponses:

62

En fonction de ce que vous voulez faire, xargs peut également vous aider (ici: convertir des 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.
Fritz G. Mehner
la source
9
Cette méthode, à mon avis, est la solution la plus élégante. Sauf que depuis que je suis paranoïaque, j'aime toujours utiliser find [...] -print0et xargs -0.
amphétamachine
7
cpus=$(getconf _NPROCESSORS_ONLN)
mr.spuratic
1
À partir du manuel, pourquoi ne pas utiliser --max-procs=0pour obtenir le plus de processus possible?
EverythingRightPlace
@EverythingRightPlace, la question ne demande explicitement pas plus de processus que de processeurs disponibles. --max-procs=0ressemble plus à la tentative du questionneur (lancer autant de processus que d'arguments).
Toby Speight le
39

Avec GNU Parallel http://www.gnu.org/software/parallel/, vous pouvez écrire:

some-command | parallel do-something

GNU Parallel prend également en charge l'exécution de travaux sur des ordinateurs distants. Cela en exécutera un par cœur de processeur sur les ordinateurs distants, même s'ils ont un nombre de cœurs différent:

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

Un exemple plus avancé: Ici, nous listons les fichiers sur lesquels nous voulons que my_script s'exécute. Les fichiers ont l'extension (peut-être .jpeg). Nous voulons que la sortie de my_script soit placée à côté des fichiers dans basename.out (par exemple foo.jpeg -> foo.out). Nous voulons exécuter my_script une fois pour chaque noyau de l'ordinateur et nous voulons également l'exécuter sur l'ordinateur local. Pour les ordinateurs distants, nous voulons que le fichier soit traité et transféré vers l'ordinateur donné. Lorsque my_script se termine, nous voulons que foo.out soit transféré et nous voulons ensuite que foo.jpeg et foo.out soient supprimés de l'ordinateur distant:

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

GNU Parallel s'assure que la sortie de chaque travail ne se mélange pas, vous pouvez donc 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

Ole Tange
la source
1
Notez que cela est vraiment utile lorsque vous utilisez une findcommande pour générer une liste de fichiers, car cela évite non seulement le problème lorsqu'il y a un espace dans un nom de fichier qui se produit for i in ...; domais find peut également faire find -name \*.extension1 -or -name \*.extension2ce que les parallèles GNU {.} Peuvent gérer très bien.
Leo Izen
Plus 1 si le catest, bien sûr, inutile.
tripleee
@tripleee Re: Utilisation inutile du chat. Voir oletange.blogspot.dk/2013/10/useless-use-of-cat.html
Ole Tange
Oh c'est toi! Incidemment, pourriez-vous mettre à jour le lien sur ce blog? L'emplacement partmaps.org est malheureusement mort, mais le redirecteur Iki devrait continuer à fonctionner.
tripleee
22
maxjobs = 4
paralléliser () {
        while [$ # -gt 0]; faire
                jobcnt = (`jobs -p`)
                if [$ {# jobcnt [@]} -lt $ maxjobs]; puis
                        faire quelque chose 1 $ &
                        décalage  
                autre
                        dormir 1
                Fi
        terminé
        attendez
}

paralléliser arg1 arg2 "5 arguments au troisième travail" arg4 ...
bstark
la source
10
Réalisez qu'il y a de sérieuses sous- citations en cours ici, de sorte que tout travail qui nécessite des espaces dans les arguments échouera gravement; de plus, ce script dévorera votre CPU en vie pendant qu'il attend la fin de certains travaux si plus de travaux sont demandés que ce que permet maxjobs.
lhunath
1
Notez également que cela suppose que votre script ne fait rien d'autre à faire avec les travaux; si vous l'êtes, il les comptera également pour maxjobs.
lhunath
1
Vous pouvez utiliser "jobs -pr" pour limiter les travaux en cours d'exécution.
amphétamachine
1
Ajout d'une commande de sommeil pour empêcher la boucle while de se répéter sans interruption, pendant qu'elle attend la fin des commandes de faire quelque chose déjà en cours d'exécution. Sinon, cette boucle occuperait essentiellement l'un des cœurs du processeur. Cela répond également aux préoccupations de @lhunath.
euphoria83
12

Voici une solution alternative qui peut être insérée dans .bashrc et utilisée pour un liner quotidien:

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

Pour l'utiliser, il suffit de mettre &après les jobs et un appel pwait, le paramètre donne le nombre de processus parallèles:

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

Ce serait plus agréable à utiliser waitau lieu d'être occupé à attendre la sortie de jobs -p, mais il ne semble pas y avoir de solution évidente pour attendre que l'un des travaux donnés soit terminé au lieu de tous.

Grumbel
la source
11

Au lieu d'un simple bash, utilisez un Makefile, puis spécifiez le nombre de travaux simultanés avec make -jX où X est le nombre de travaux à exécuter à la fois.

Ou vous pouvez utiliser wait(" man wait"): lancez plusieurs processus enfants, appelez wait- il se fermera lorsque les processus enfants seront terminés.

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 le résultat de la tâche, affectez leur résultat à une variable. Après avoir waitvérifié ce que contient la variable.

Skolima
la source
1
Merci pour cela, même si le code n'est pas terminé, il m'a donné la réponse à un problème que j'ai au travail.
gerikson le
le seul problème est que si vous tuez le script de premier plan (celui avec la boucle) les emplois qui ont été en cours d' exécution ne seront pas tués ensemble
Girardi
8

Essayez peut-être un utilitaire de parallélisation au lieu de réécrire la boucle? Je suis un grand fan de xjobs. J'utilise xjobs tout le temps pour copier en masse des fichiers sur 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

Tessein
la source
7

Si vous êtes familier avec la makecommande, la plupart du temps, vous pouvez exprimer la liste des commandes que vous souhaitez exécuter en tant que fichier Make. Par exemple, si vous devez exécuter $ SOME_COMMAND sur des fichiers * .input dont chacun produit * .output, vous pouvez utiliser le makefile

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

%.sortie entrée
    $ (SOME_COMMAND) $ <$ @

tout: $ (OUTPUT)

et puis juste courir

make -j <NOMBRE>

pour exécuter au maximum NUMBER commandes en parallèle.

Idélique
la source
6

Bien qu'il bashsoit probablement impossible de faire cela directement , vous pouvez faire un semi-droit assez facilement. bstarka donné une bonne approximation du droit mais le sien présente les défauts suivants:

  • Fractionnement de mots: vous ne pouvez pas lui transmettre de tâches qui utilisent l'un des caractères suivants dans leurs arguments: espaces, tabulations, retours à la ligne, étoiles, points d'interrogation. Si vous le faites, les choses vont casser, peut-être de manière inattendue.
  • Il repose sur le reste de votre script pour ne rien faire d'arrière-plan. Si vous le faites, ou plus tard vous ajoutez quelque chose au script qui est envoyé en arrière-plan parce que vous avez oublié que vous n'étiez pas autorisé à utiliser des travaux en arrière-plan à cause de son extrait de code, les choses vont casser.

Une autre approximation 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 pour vérifier également le code de sortie de chaque travail à la fin afin que vous puissiez avertir l'utilisateur si un travail échoue ou définir un code de sortie pour en scheduleAllfonction du nombre de travaux qui ont échoué, ou quelque chose.

Le problème avec ce code est juste que:

  • Il planifie quatre (dans ce cas) travaux à la fois, puis attend la fin des quatre. Certaines peuvent être effectuées plus tôt que d'autres, ce qui entraînera l'attente du prochain lot de quatre travaux jusqu'à ce que le plus long du lot précédent soit terminé.

Une solution qui prend en charge ce dernier problème devrait utiliser kill -0pour interroger si l'un des processus a disparu au lieu de waitet planifier le travail suivant. Cependant, cela introduit un petit problème nouveau: vous avez une condition de concurrence entre la fin d'un travail et la kill -0vérification de sa fin. Si le travail se termine et qu'un autre processus sur votre système démarre en même temps, en prenant un PID aléatoire qui se trouve être celui du travail qui vient de se terminer, le kill -0ne remarquera pas que votre travail est terminé et les choses se casseront à nouveau.

Une solution parfaite n'est pas possible dans bash.

lhunath
la source
3

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
}

en utilisant:

cat my_commands | parallel -j 4
ilnar
la source
L'utilisation de make -jest intelligente, mais sans aucune explication et cette goutte de code Awk en écriture seule, je m'abstiens de voter.
tripleee
2

Le projet sur lequel je travaille utilise la commande wait pour contrôler les processus shell parallèles (ksh en fait). Pour répondre à vos préoccupations concernant les E / S, sur un système d'exploitation moderne, il est possible qu'une exécution parallèle augmente réellement l'efficacité. Si tous les processus lisent les mêmes blocs sur le disque, seul le premier processus devra atteindre le matériel physique. Les autres processus pourront souvent récupérer le bloc du cache disque du système d'exploitation en mémoire. De toute évidence, la lecture à partir de la mémoire est plusieurs ordres de grandeur plus rapide que la lecture à partir du disque. En outre, l'avantage ne nécessite aucune modification de codage.

Jon Ericson
la source
1

Cela peut être suffisant pour la plupart des applications, mais ce n'est pas 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
chat
la source
1

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
Fernando
la source
1

Vraiment en retard à la fête ici, mais voici une autre solution.

Beaucoup de solutions ne gèrent pas les espaces / caractères spéciaux dans les commandes, ne font pas fonctionner N jobs à tout moment, mangent le processeur dans des boucles occupées ou s'appuient sur des dépendances externes (par exemple GNU parallel).

Avec l' inspiration pour la gestion des processus morts / zombies , voici une solution pure bash:

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

Le résultat:

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

La gestion de la sortie par processus $$peut être utilisée 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[@]}"

Production:

1 56871
2 56872
Skrat
la source
0

Vous pouvez utiliser une simple boucle for imbriquée (remplacez les entiers appropriés par N et M ci-dessous):

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

Cela exécutera do_something N * M fois en M tours, chaque tour exécutant N travaux en parallèle. Vous pouvez rendre N égal au nombre de processeurs dont vous disposez.

Adam Zalcman
la source
0

Ma solution pour toujours garder un nombre donné de processus en cours d'exécution, suivre les erreurs et gérer les processus ubnterruptibles / zombies:

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
}

Usage:

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
la source
-1

$ DOMAINS = "liste de certains domaines dans les commandes" pour foo in some-command do

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

terminé

Ndomaines =echo $DOMAINS |wc -w

for i in $ (seq 1 1 $ Ndomains) do echo "wait for $ {job [$ i]}" wait "$ {job [$ i]}" done

dans ce concept fonctionnera pour la parallélisation. la chose importante est que la dernière ligne de eval est «&» qui mettra les commandes aux arrière-plans.

Jack
la source