utilisation parallèle pour traiter des fichiers d'entrée uniques en fichiers de sortie uniques

18

J'ai un problème de script shell où je reçois un répertoire plein de fichiers d'entrée (chaque fichier contenant de nombreuses lignes d'entrée), et je dois les traiter individuellement, en redirigeant chacune de leurs sorties vers un fichier unique (aka, file_1.input needs à capturer dans file_1.output, etc.).

Pré-parallèle , je voudrais simplement parcourir chaque fichier du répertoire et exécuter ma commande, tout en faisant une sorte de minuterie / technique de comptage pour ne pas submerger les processeurs (en supposant que chaque processus a un temps d'exécution constant). Cependant, je sais que ce ne sera pas toujours le cas, donc l'utilisation d'une solution de type "parallèle" semble être le meilleur moyen d'obtenir le multi-threading du script shell sans écrire de code personnalisé.

Bien que j'aie pensé à quelques façons de fouetter en parallèle pour traiter chacun de ces fichiers (et me permettre de gérer mes cœurs efficacement), ils semblent tous hacky. J'ai ce que je pense être un cas d'utilisation assez facile, je préférerais donc le garder aussi propre que possible (et rien dans les exemples parallèles ne semble ressortir comme étant mon problème.

Toute aide serait appréciée!

exemple de répertoire d'entrée:

> ls -l input_files/
total 13355
location1.txt
location2.txt
location3.txt
location4.txt
location5.txt

Scénario:

> cat proces_script.sh
#!/bin/sh

customScript -c 33 -I -file [inputFile] -a -v 55 > [outputFile]

Mise à jour : Après avoir lu la réponse d'Ole ci-dessous, j'ai pu rassembler les pièces manquantes pour ma propre implémentation parallèle. Bien que sa réponse soit excellente, voici mes recherches supplémentaires et les notes que j'ai prises:

Au lieu d'exécuter mon processus complet, j'ai pensé commencer par une commande de preuve de concept pour prouver sa solution dans mon environnement. Voir mes deux implémentations différentes (et notes):

find /home/me/input_files -type f -name *.txt | parallel cat /home/me/input_files/{} '>' /home/me/output_files/{.}.out

Utilise find (pas ls, qui peut entraîner des problèmes) pour rechercher tous les fichiers applicables dans mon répertoire de fichiers d'entrée, puis redirige leur contenu vers un répertoire et un fichier séparés. Mon problème d'en haut était la lecture et la redirection (le script réel était simple), donc remplacer le script par cat était une bonne preuve de concept.

parallel cat '>' /home/me/output_files/{.}.out :::  /home/me/input_files/*

Cette deuxième solution utilise le paradigme de variable d'entrée parallèle pour lire les fichiers, mais pour un novice, c'était beaucoup plus déroutant. Pour moi, l'utilisation de find a and pipe a très bien répondu à mes besoins.

J Jones
la source

Réponses:

27

GNU Parallel est conçu pour ce type de tâches:

parallel customScript -c 33 -I -file {} -a -v 55 '>' {.}.output ::: *.input

ou:

ls | parallel customScript -c 33 -I -file {} -a -v 55 '>' {.}.output

Il exécutera une tâche par cœur de processeur.

Vous pouvez installer GNU Parallel simplement en:

wget https://git.savannah.gnu.org/cgit/parallel.git/plain/src/parallel
chmod 755 parallel
cp parallel sem

Regardez les vidéos d'introduction pour GNU Parallel pour en savoir plus: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Ole Tange
la source
Excellente réponse (et principaux points pour lire ma demande d'utilisation du parallèle).
J Jones
5

La méthode standard consiste à configurer une file d'attente et à générer un nombre illimité de travailleurs qui savent comment extraire quelque chose de la file d'attente et le traiter. Vous pouvez utiliser un fifo (alias nommé pipe) pour la communication entre ces processus.

Voici un exemple naïf pour démontrer le concept.

Un script de file d'attente simple:

#!/bin/sh
mkfifo /tmp/location-queue
for i in inputfiles/*; do
  echo $i > /tmp/location-queue
done
rm /tmp/location-queue

Et un travailleur:

#!/bin/sh
while read file < /tmp/location-queue; do
  process_file "$file"
done

process_file pourrait être défini quelque part dans votre travailleur, et il peut faire tout ce que vous en avez besoin.

Une fois que vous disposez de ces deux éléments, vous pouvez disposer d'un simple moniteur qui démarre le processus de file d'attente et n'importe quel nombre de processus de travail.

Script de moniteur:

#!/bin/sh
queue.sh &
num_workers="$1"
i=0
while [ $i < $num_workers ]; do
  worker.sh &
  echo $! >> /tmp/worker.pids
  i=$((i+1))
done
monitor_workers

Voilà. Si vous faites cela, il est préférable de configurer le fifo sur le moniteur et de transmettre le chemin d'accès à la file d'attente et aux travailleurs, afin qu'ils ne soient pas couplés et ne soient pas collés à un emplacement spécifique pour le fifo. Je l'ai configuré de cette façon dans la réponse spécifiquement, il est donc clair que ce que vous utilisez lorsque vous le lisez.

Shawn J. Goff
la source
Comment le moniteur est-il suffisamment intelligent pour suspendre le frai sur les nouveaux employés jusqu'à la fin du prochain (alias, où $ i est-il décrémenté)? ---- Répondant à ma propre modification, les travailleurs ne partent jamais, ils traitent simplement les fichiers jusqu'à ce que tout le traitement soit épuisé (d'où la boucle while dans les «processeurs» également).
J Jones
Quelle est la ligne "monitor_workers" à la fin du script de moniteur?
J Jones
@JJones - monitor_workersc'est comme process_file- c'est une fonction qui fait ce que vous voulez. À propos du moniteur - vous aviez raison; il devrait sauver les pids de ses ouvriers (afin qu'il puisse envoyer un signal d'arrêt) et le compteur doit être incrémenté lorsqu'il démarre un ouvrier. J'ai édité la réponse pour l'inclure.
Shawn J. Goff
J'apprécie vraiment votre travail, mais je pense que vous devriez utiliser les GNU parallel. Je pense que c'est votre idée, pleinement mise en œuvre.
motobói
5

Un autre exemple:

ls *.txt | parallel 'sort {} > {.}.sorted.txt'

J'ai trouvé les autres exemples inutilement complexes, alors que dans la plupart des cas, c'est ce que vous cherchiez peut-être.

caviar décéléré
la source
4

Un outil couramment disponible qui peut faire la parallélisation est make. GNU make et quelques autres ont un-j possibilité d'effectuer des builds parallèles.

.SUFFIXES: .input .output
.input.output:
        process_one_file <$< >[email protected]
        mv -f [email protected] $@

Exécutez makecomme ceci (je suppose que les noms de vos fichiers ne contiennent pas de caractères spéciaux, ce maken'est pas bon avec ceux-ci):

make -j 4 $(for x in *.input; do echo ${x%.*}.output; done)
Gilles 'SO- arrête d'être méchant'
la source
à mon humble avis, c'est la solution la plus intelligente :)
h4unt3r
3

C'est pour exécuter la même commande sur un grand ensemble de fichiers dans le répertoire courant:

#!/bin/sh
trap 'worker=`expr $worker - 1`' USR1  # free up a worker
worker=0  # current worker
num_workers=10  # maximum number of workers
for file in *.txt; do
    if [ $worker -lt $num_workers ]; then
        {   customScript -c 33 -I -file $file -a -v 55 > `basename $file .txt`.outtxt 
            kill -USR1 $$ 2>/dev/null  # signal parent that we're free
        } &
        echo $worker/$num_worker $! $file  # feedback to caller
        worker=`expr $worker + 1`
    else
        wait # for a worker to finish
    fi
done

Cela exécute le customScriptsur chaque txtfichier, en mettant la sortie dans des outtxtfichiers. Changez selon vos besoins. La clé pour que cela fonctionne est le traitement du signal, en utilisant SIGUSR1 afin que le processus enfant puisse faire savoir au processus parent que c'est fait. L'utilisation de SIGCHLD ne fonctionnera pas car la plupart des instructions du script généreront des signaux SIGCHLD vers le script shell. J'ai essayé ceci en remplaçant votre commande par sleep 1, le programme a utilisé 0,28s de CPU utilisateur et 0,14s de CPU système; ce n'était que sur environ 400 fichiers.

Arcege
la source
Dans quelle mesure l'attente est-elle suffisamment intelligente pour reprendre le même fichier en cours d'itération et saisir à nouveau l'instruction "si"?
J Jones
Ce n'est pas ce waitqui est suffisamment «intelligent»; mais il reviendra après avoir reçu le SIGUSR1signal. L'enfant / travailleur envoie un SIGUSR1au parent, qui est pris ( trap), et décrémente $worker( trapclause) et revient anormalement de wait, permettant à la if [ $worker -lt $num_workers ]clause de s'exécuter.
Arcege
0

Ou utilisez simplement xargs -P, pas besoin d'installer de logiciel supplémentaire:

find . -type f -print0 | xargs -0 -I'XXX' -P4 -n1 custom_script -input "XXX" -output "XXX.out"

Un peu d'explication pour les options:

  • -I'XXX' définit la chaîne qui sera remplacée dans le modèle de commande par le nom de fichier
  • -P4 exécutera 4 processus en parallèle
  • -n1 ne mettra qu'un seul fichier par exécution même si deux XXX sont trouvés
  • -print0et -0travailler ensemble, vous permettant d'avoir des caractères spéciaux (comme des espaces) dans les noms de fichiers
Piotr Czapla
la source