Quatre tâches en parallèle… comment faire?

23

J'ai un tas d'images PNG sur un répertoire. J'ai une application appelée pngout que je lance pour compresser ces images. Cette application est appelée par un script que j'ai fait. Le problème est que ce script fait un à la fois, quelque chose comme ceci:

FILES=(./*.png)
for f in  "${FILES[@]}"
do
        echo "Processing $f file..."
        # take action on each file. $f store current file name
        ./pngout -s0 $f R${f/\.\//}
done

Le traitement d'un seul fichier à la fois prend beaucoup de temps. Après avoir exécuté cette application, je constate que le CPU n'est que de 10%. J'ai donc découvert que je pouvais diviser ces fichiers en 4 lots, placer chaque lot dans un répertoire et en lancer 4, à partir de quatre fenêtres de terminal, quatre processus, donc j'ai quatre instances de mon script, en même temps, le traitement de ces images et la le travail prend 1/4 du temps.

Le deuxième problème est que j'ai perdu du temps à diviser les images et les lots et à copier le script dans quatre répertoires, ouvrir 4 fenêtres de terminal, bla bla ...

Comment faire avec un script, sans avoir à diviser quoi que ce soit?

Je veux dire deux choses: d'abord comment puis-je, à partir d'un script bash, déclencher un processus en arrière-plan? (ajoutez simplement & à la fin?) Deuxièmement: comment puis-je arrêter d'envoyer des tâches en arrière-plan après avoir envoyé les quatrièmes tâches et mettre le script en attente jusqu'à la fin des tâches? Je veux dire, envoyer simplement une nouvelle tâche en arrière-plan à la fin d'une tâche, en gardant toujours 4 tâches en parallèle? si je ne le fais pas, la boucle déclenchera des millions de tâches en arrière-plan et le CPU se bouchera.

SpaceDog
la source
Voir aussi Parallelizing a for loop
Gilles 'SO- stop being evil'

Réponses:

33

Si vous en avez une copie xargsqui prend en charge l'exécution parallèle avec -P, vous pouvez simplement faire

printf '%s\0' *.png | xargs -0 -I {} -P 4 ./pngout -s0 {} R{}

Pour d'autres idées, le wiki Wooledge Bash a une section dans l'article Gestion des processus décrivant exactement ce que vous voulez.

jw013
la source
2
Il existe également des "gnu parallel" et des "xjobs" conçus pour ce cas. C'est surtout une question de goût que vous préférez.
wnoise
Pourriez-vous expliquer la commande proposée? Merci!
Eugene S
1
@EugeneS Pourriez-vous être un peu plus précis sur quelle partie? Le printf collecte tous les fichiers png et les transmet via un canal à xargs, qui collecte les arguments de l'entrée standard et les combine en arguments pour la pngoutcommande que l'OP voulait exécuter. L'option clé est -P 4, qui indique à xargs d'utiliser jusqu'à 4 commandes simultanées.
jw013
2
Désolé de ne pas être précis. J'étais particulièrement intéressé par la raison pour laquelle vous utilisiez la printffonction ici plutôt que simplement régulière ls .. | grep .. *.png? J'ai également été intéressé par les xargsparamètres que vous avez utilisés ( -0et -I{}). Merci!
Eugene S
3
@EugeneS C'est pour une exactitude et une robustesse maximales. Les noms de fichiers ne sont pas des lignes et lsne peuvent pas être utilisés pour analyser les noms de fichiers de manière portative et en toute sécurité . Les seuls caractères sûrs à utiliser pour délimiter les noms de fichiers sont \0et /, puisque tous les autres caractères, y compris \n, peuvent faire partie du nom de fichier lui-même. Les printfutilisations \0pour délimiter les noms de fichiers, et les -0informe xargsde cela. Le -I{}dit xargsde remplacer {}par l'argument.
jw013
8

En plus des solutions déjà proposées, vous pouvez créer un makefile qui décrit comment créer un fichier compressé à partir d'un fichier non compressé, et l'utiliser make -j 4pour exécuter 4 travaux en parallèle. Le problème est que vous devrez nommer les fichiers compressés et non compressés différemment, ou les stocker dans des répertoires différents, sinon il sera impossible d'écrire une règle de création raisonnable.

9000
la source
5

Pour répondre à vos deux questions:

  • oui, l'ajout de & à la fin de la ligne vous demandera de lancer un processus d'arrière-plan.
  • à l'aide de la waitcommande, vous pouvez demander au shell d'attendre la fin de tous les processus en arrière-plan avant de poursuivre.

Voici le script modifié afin qu'il jsoit utilisé pour garder une trace du nombre de processus d'arrière-plan. Une fois NB_CONCURRENT_PROCESSESatteint, le script sera réinitialisé jà 0 et attendra la fin de tous les processus d'arrière-plan avant de reprendre son exécution.

files=(./*.png)
nb_concurrent_processes=4
j=0
for f in "${files[@]}"
do
        echo "Processing $f file..."
        # take action on each file. $f store current file name
        ./pngout -s0 "$f" R"${f/\.\//}" &
        ((++j == nb_concurrent_processes)) && { j=0; wait; }
done
Frederik Deweerdt
la source
1
Cela attendra le dernier des quatre processus simultanés et démarrera ensuite un ensemble de quatre autres. Peut-être faudrait-il construire un tableau de quatre PID, puis attendre ces PID spécifiques?
Nils
Juste pour expliquer mes correctifs au code: (1) Pour des raisons de style, évitez tous les noms de variables majuscules car ils sont potentiellement en conflit avec les variables internes du shell. (2) Ajout de citations pour $fetc. (3) Utilisation [pour les scripts compatibles POSIX, mais pour le bash pur [[est toujours préféré. Dans ce cas, ((est plus approprié pour l'arithmétique.
jw013