Tareas en paralelo en Bash: &, wait, wait -n y xargs -P

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.

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 &
wait
salidaB (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 & y waitPocas tareas distintas a la vezwait sin argumentos no informa de errores
  • wait 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 -0 con find -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.