Ejecutando un número limitado de procesos hijo en paralelo en bash?

Tengo un gran conjunto de archivos para los cuales es necesario realizar un gran procesamiento. Este procesamiento en una sola hebra, utiliza unos cientos de MiB de RAM (en la máquina utilizada para comenzar el trabajo) y tarda unos minutos en ejecutarse. Mi uso actual es comenzar un trabajo de hadoop en los datos de entrada, pero he tenido el mismo problema en otros casos anteriormente.

Para poder utilizar completamente la potencia de CPU disponible, quiero poder ejecutar varias de esas tareas en paralelo.

Sin embargo, un script de shell de ejemplo muy simple como este arruinará el rendimiento del sistema debido a una carga e intercambio excesivos:

find . -type f | while read name ; do some_heavy_processing_command ${name} & done 

Entonces, lo que quiero es esencialmente similar a lo que hace “gmake -j4”.

Sé que bash admite el comando “esperar”, pero eso solo espera hasta que se completen todos los procesos secundarios. En el pasado he creado scripts que hacen un comando ‘ps’ y luego grep el niño procesa por su nombre (sí, lo sé … feo).

¿Cuál es la solución más simple / limpia / mejor para hacer lo que quiero?


Editar: Gracias a Frederik: Sí, de hecho, este es un duplicado de Cómo limitar el número de subprocesos / procesos utilizados en una función en bash. El “xargs –max-procs = 4” funciona como un amuleto. (Así que voté para cerrar mi propia pregunta)

 #! /usr/bin/env bash set -o monitor # means: run background processes in a separate processes... trap add_next_job CHLD # execute add_next_job when we receive a child complete signal todo_array=($(find . -type f)) # places output into an array index=0 max_jobs=2 function add_next_job { # if still jobs to do then add one if [[ $index -lt ${#todo_array[*]} ]] # apparently stackoverflow doesn't like bash syntax # the hash in the if is not a comment - rather it's bash awkward way of getting its length then echo adding job ${todo_array[$index]} do_job ${todo_array[$index]} & # replace the line above with the command you want index=$(($index+1)) fi } function do_job { echo "starting job $1" sleep 2 } # add initial set of jobs while [[ $index -lt $max_jobs ]] do add_next_job done # wait for all jobs to complete wait echo "done" 

Habiendo dicho eso, Fredrik hace la excelente observación de que xargs hace exactamente lo que quieres …

Sé que llego tarde a la fiesta con esta respuesta, pero pensé que publicaría una alternativa que, en mi humilde opinión, simplifica y simplifica el cuerpo de la secuencia de comandos. (Claramente, puede cambiar los valores 2 y 5 para que sean apropiados para su escenario).

 function max2 { while [ `jobs | wc -l` -ge 2 ] do sleep 5 done } find . -type f | while read name ; do max2; some_heavy_processing_command ${name} & done wait 

Con GNU Parallel se vuelve más simple:

 find . -type f | parallel some_heavy_processing_command {} 

Obtenga más información: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1.

Creo que encontré una solución más práctica usando make :

 #!/usr/bin/make -f THIS := $(lastword $(MAKEFILE_LIST)) TARGETS := $(shell find . -name '*.sh' -type f) .PHONY: all $(TARGETS) all: $(TARGETS) $(TARGETS): some_heavy_processing_command $@ $(THIS): ; # Avoid to try to remake this makefile 

Llámalo como, por ejemplo, ‘test.mak’, y agrega derechos de ejecución. Si llama a ./test.mak , llamará al some_heavy_processing_command uno por uno. Pero puede llamar como ./test.mak -j 4 , luego ejecutará cuatro subprocesos a la vez. También puede usarlo de una manera más sofisticada: ejecutar como ./test.mak -j 5 -l 1.5 , luego ejecutará un máximo de 5 subprocesos mientras la carga del sistema sea inferior a 1.5, pero limitará la cantidad de procesos si la carga del sistema excede 1.5.

Es más flexible que xargs , y make es parte de la distribución estándar, no como parallel .

Este código funcionó bastante bien para mí.

Me di cuenta de un problema en el que el guión no podía terminar. Si se encuentra con un caso donde el script no finalizará debido a que max_jobs es mayor que la cantidad de elementos en el conjunto, el script nunca se cerrará.

Para evitar el escenario anterior, agregué lo siguiente justo después de la statement “max_jobs”.

 if [ $max_jobs -gt ${#todo_array[*]} ]; then # there are more elements found in the array than max jobs, setting max jobs to #of array elements" max_jobs=${#todo_array[*]} fi 

Otra opción:

 PARALLEL_MAX=... function start_job() { while [ $(ps --no-headers -o pid --ppid=$$ | wc -l) -gt $PARALLEL_MAX ]; do sleep .1 # Wait for background tasks to complete. done "$@" & } start_job some_big_command1 start_job some_big_command2 start_job some_big_command3 start_job some_big_command4 ... 

Aquí hay una función muy buena que utilicé para controlar el máximo de # trabajos de bash o ksh. NOTA: el – 1 en el pgrep resta el subproceso wc -l.

 function jobmax { typeset -i MAXJOBS=$1 sleep .1 while (( ($(pgrep -P $$ | wc -l) - 1) >= $MAXJOBS )) do sleep .1 done } nproc=5 for i in {1..100} do sleep 1 & jobmax $nproc done wait # Wait for the rest