bash - programas - scripts linux ejercicios resueltos
Espere a que terminen los trabajos en segundo plano de bash en script (4)
Para maximizar el uso de la CPU (ejecuto cosas en Debian Lenny en EC2) tengo un script simple para iniciar trabajos en paralelo:
#!/bin/bash
for i in apache-200901*.log; do echo "Processing $i ..."; do_something_important; done &
for i in apache-200902*.log; do echo "Processing $i ..."; do_something_important; done &
for i in apache-200903*.log; do echo "Processing $i ..."; do_something_important; done &
for i in apache-200904*.log; do echo "Processing $i ..."; do_something_important; done &
...
Estoy bastante satisfecho con esta solución de trabajo, sin embargo, no pude entender cómo escribir más código que solo se ejecutó una vez que se completaron todos los bucles.
¿Hay alguna forma de controlar esto?
El uso de GNU Parallel hará que su secuencia de comandos sea aún más corta y posiblemente más eficiente:
parallel ''echo "Processing "{}" ..."; do_something_important {}'' ::: apache-*.log
Esto ejecutará un trabajo por núcleo de CPU y continuará haciéndolo hasta que se procesen todos los archivos.
Su solución básicamente dividirá los trabajos en grupos antes de ejecutar. Aquí 32 trabajos en 4 grupos:
En cambio, GNU Parallel genera un nuevo proceso cuando uno finaliza, manteniendo las CPU activas y ahorrando tiempo:
Aprender más:
- Mire el video introductorio para obtener una introducción rápida: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1
- Recorre el tutorial (man parallel_tutorial). Tu línea de comando te amará por eso.
Esta es mi solución cruda:
function run_task {
cmd=$1
output=$2
concurency=$3
if [ -f ${output}.done ]; then
# experiment already run
echo "Command already run: $cmd. Found output $output"
return
fi
count=`jobs -p | wc -l`
echo "New active task #$count: $cmd > $output"
$cmd > $output && touch $output.done &
stop=$(($count >= $concurency))
while [ $stop -eq 1 ]; do
echo "Waiting for $count worker threads..."
sleep 1
count=`jobs -p | wc -l`
stop=$(($count > $concurency))
done
}
La idea es usar "trabajos" para ver cuántos niños están activos en segundo plano y esperar hasta que este número disminuya (un niño sale). Una vez que existe un niño, la siguiente tarea se puede iniciar.
Como puede ver, también hay un poco de lógica adicional para evitar ejecutar los mismos experimentos / comandos varias veces. Hace el trabajo por mí. Sin embargo, esta lógica podría omitirse o mejorarse más (por ejemplo, verificar las marcas de tiempo de creación de archivos, los parámetros de entrada, etc.).
Hay un comando bash
incorporado para eso.
wait [n ...]
Wait for each specified process and return its termination sta‐
tus. Each n may be a process ID or a job specification; if a
job spec is given, all processes in that job’s pipeline are
waited for. If n is not given, all currently active child pro‐
cesses are waited for, and the return status is zero. If n
specifies a non-existent process or job, the return status is
127. Otherwise, the return status is the exit status of the
last process or job waited for.
Tuve que hacer esto recientemente y terminé con la siguiente solución:
while true; do
wait -n || {
code="$?"
([[ $code = "127" ]] && exit 0 || exit "$code")
break
}
done;
Así es como funciona:
wait -n
cierra tan pronto como uno de los (potencialmente muchos) trabajos en segundo plano finaliza. Siempre se evalúa como verdadero y el ciclo continúa hasta que:
- Código de salida
127
: el último trabajo de fondo salió exitosamente. En ese caso, ignoramos el código de salida y salimos del subconjunto con el código 0. - Cualquiera de los trabajos en segundo plano falló. Simplemente salimos del subconjunto con ese código de salida.
Con set -e
, esto garantizará que el script finalice antes y pase por el código de salida de cualquier trabajo de fondo fallido.