Cómo ejecutar varias tareas en paralelo en un script de Bash para que termine antes: lanzarlas en segundo plano y esperarlas, saber cuál falló, recoger sus resultados, no tener más de N en marcha a la vez y procesar listas largas de archivos con xargs -P.
También te puede interesar
Lanzar en segundo plano con & y esperar con wait
Una orden seguida de & se ejecuta en segundo plano: Bash la lanza y sigue con la siguiente línea sin esperar a que termine. wait, sin nada más, espera a que acaben todas. En este ejemplo cada «tarea» simula un trabajo lento con sleep; la variable SECONDS de Bash cuenta los segundos transcurridos:
#!/usr/bin/env bash
tarea() {
sleep 1 # aquí iría el trabajo de verdad: descargar, comprimir, convertir...
}
SECONDS=0
for n in 1 2 3 4; do
tarea "$n"
done
echo "Cuatro tareas, una detrás de otra: $SECONDS s"
SECONDS=0
for n in 1 2 3 4; do
tarea "$n" &
done
wait
echo "Cuatro tareas en paralelo: $SECONDS s"salidaCuatro tareas, una detrás de otra: 4 s Cuatro tareas en paralelo: 1 s
Cuatro tareas de un segundo tardan cuatro segundos en fila y uno en paralelo. Pero ojo: las tareas en paralelo terminan en el orden que les toque, no en el que las lanzaste. Si cada una tarda distinto, su salida llega desordenada:
#!/usr/bin/env bash
tarea() {
sleep "$2"
echo "$1 (tardó $2 s)"
}
tarea A 0.6 &
tarea B 0.2 &
tarea C 0.8 &
tarea D 0.4 &
waitsalidaB (tardó 0.2 s) D (tardó 0.4 s) A (tardó 0.6 s) C (tardó 0.8 s)
Saber si alguna falló
wait sin argumentos devuelve siempre 0, aunque alguna tarea haya fallado. Para conocer el código de salida de cada una, guarda su número de proceso (lo deja Bash en $! justo después del &) y espera por cada uno con wait PID, que devuelve el código de salida de ese proceso:
#!/usr/bin/env bash
procesar() {
case $1 in documentos) sleep 0.2 ;; fotos) sleep 0.4 ;; *) sleep 0.6 ;; esac
if [[ $1 == fotos ]]; then
echo "$1: error, disco lleno" >&2
return 1
fi
echo "$1: correcto"
}
declare -A pids
for carpeta in documentos fotos musica; do
procesar "$carpeta" &
pids[$carpeta]=$!
done
fallos=0
for carpeta in documentos fotos musica; do
if ! wait "${pids[$carpeta]}"; then
echo "Falló: $carpeta"
(( fallos += 1 ))
fi
done
echo "Tareas con error: $fallos"
exit $(( fallos > 0 ))salida con errordocumentos: correcto fotos: error, disco lleno Falló: fotos musica: correcto Tareas con error: 1 código de salida: 1
Las variables no vuelven de segundo plano
Cada tarea con & se ejecuta en una copia del script (una subshell). Lo que cambie dentro no se ve fuera: este contador se queda en 0. Para recoger resultados, haz que cada tarea los escriba en su propio archivo y júntalos al final:
#!/usr/bin/env bash
total=0
for n in 1 2 3; do
{ (( total += n )); } &
done
wait
echo "total = $total (no funciona: cada suma ocurrió en otra copia del script)"
resultados=$(mktemp -d)
for n in 1 2 3; do
{ echo $(( n * n )) > "$resultados/$n"; } &
done
wait
suma=0
for f in "$resultados"/*; do
(( suma += $(< "$f") ))
done
echo "suma de cuadrados = $suma"
rm -r -- "$resultados"salidatotal = 0 (no funciona: cada suma ocurrió en otra copia del script) suma de cuadrados = 14
Este error ni siquiera hace falta ejecutarlo para verlo: ShellCheck avisa de que la suma ocurre en una subshell y de que el cambio se pierde:
shellcheckIn variables.sh line 4:
{ (( total += n )); } &
^-- SC2030 (info): Modification of total is local (to subshell caused by backgrounding &).
In variables.sh line 7:
echo "total = $total (no funciona: cada suma ocurrió en otra copia del script)"
^----^ SC2031 (info): total was modified in a subshell. That change might be lost.
For more information:
https://www.shellcheck.net/wiki/SC2030 -- Modification of total is local (t...
https://www.shellcheck.net/wiki/SC2031 -- total was modified in a subshell....No más de N a la vez
Lanzar cien tareas de golpe puede saturar el equipo o el servidor al que llaman. wait -n (Bash 4.3 o posterior) espera a que termine una cualquiera, lo que permite mantener un máximo de tareas en marcha. jobs -rp lista las que siguen corriendo:
#!/usr/bin/env bash
maximo=2
registro=$(mktemp)
tarea() {
sleep 0.3
echo "archivo-$1 convertido" >> "$registro"
}
pico=0
for n in {1..6}; do
while (( $(jobs -rp | wc -l) >= maximo )); do
wait -n
done
tarea "$n" &
activas=$(jobs -rp | wc -l)
(( activas > pico )) && pico=$activas
done
wait
sort "$registro"
echo "Nunca hubo más de $pico tareas a la vez"
rm -- "$registro"salidaarchivo-1 convertido archivo-2 convertido archivo-3 convertido archivo-4 convertido archivo-5 convertido archivo-6 convertido Nunca hubo más de 2 tareas a la vez
xargs -P: paralelo en una línea
Para aplicar la misma orden a muchos archivos, xargs -P N hace todo lo anterior por ti: lee la lista, lanza hasta N procesos a la vez y espera a que acaben. Con find … -print0 y xargs -0 los nombres viajan separados por un carácter nulo y funcionan aunque tengan espacios. Si alguno falla, xargs termina con el código 123:
#!/usr/bin/env bash
# shellcheck disable=SC2016 # las comillas simples son a propósito: $1 lo expande el bash que lanza xargs
mkdir -p entrada
for n in 1 2 3 4 5; do
seq -f 'línea %g' $(( n + 1 )) > "entrada/archivo $n.txt"
done
contar() {
local lineas
lineas=$(wc -l < "$1") || return 1
printf '%s: %s líneas\n' "${1##*/}" "$lineas"
}
export -f contar
find entrada -name '*.txt' -print0 |
xargs -0 -P 3 -I{} bash -c 'contar "$1"' _ {} |
sort
# Con un archivo que no se puede leer
touch "entrada/archivo 6.txt" && chmod 000 "entrada/archivo 6.txt"
find entrada -name '*.txt' -print0 |
xargs -0 -P 3 -I{} bash -c 'contar "$1"' _ {} > /dev/null
echo "código de salida de xargs: $?"salidaarchivo 1.txt: 2 líneas archivo 2.txt: 3 líneas archivo 3.txt: 4 líneas archivo 4.txt: 5 líneas archivo 5.txt: 6 líneas environment: line 1: entrada/archivo 6.txt: Permission denied código de salida de xargs: 123
Fíjate en la forma bash -c 'contar "$1"' _ {}: el nombre del archivo llega como argumento $1, y no pegado dentro del texto de la orden. Escribir bash -c "contar {}" parece lo mismo, pero un archivo llamado $(rm -rf ~).txt se ejecutaría como código. La salida se ordena con sort al final porque, como antes, cada proceso escribe cuando termina. El mensaje del archivo sin permiso empieza por environment porque el error ocurre dentro de contar, una función que llegó al nuevo Bash a través del entorno con export -f.
- HerramientaPara quéOjo con
orden &ywaitPocas tareas distintas a la vezwaitsin argumentos no informa de erroreswait PIDEl código de salida de cada tareaGuarda$!justo después del&wait -nUn máximo de tareas en marchaBash 4.3 o posterior (macOS trae la 3.2)xargs -P NLa misma orden sobre una lista largaUsa-0confind -print0- GNU parallelLo mismo que xargs con más opciones (reintentos, salida ordenada)No viene instalado:
apt install parallel
Todos los scripts de esta página se ejecutaron con Bash 5.3 en Ubuntu 26.04, en un contenedor limpio sin acceso a la red, y pasaron ShellCheck 0.11 sin avisos antes de publicarla; las salidas están copiadas de esa ejecución.