Bash: ¿limita el número de trabajos simultáneos?

¿Existe alguna manera fácil de limitar el número de trabajos concurrentes en bash? Con eso me refiero a hacer el & bloque cuando hay más de n trabajos concurrentes ejecutándose en segundo plano.

Sé que puedo implementar esto con ps | trucos estilo grep, pero ¿hay una manera más fácil?

Si tienes GNU Parallel http://www.gnu.org/software/parallel/ instalado, puedes hacer esto:

parallel gzip ::: *.log 

que ejecutará un gzip por núcleo de CPU hasta que todos los archivos de registro tengan gzip.

Si es parte de un ciclo mayor, puede usar sem lugar:

 for i in *.log ; do echo $i Do more stuff here sem -j+0 gzip $i ";" echo done done sem --wait 

Hará lo mismo, pero le dará la oportunidad de hacer más cosas para cada archivo.

Si GNU Parallel no está empaquetado para su distribución, puede instalar GNU Parallel simplemente por:

 (wget -O - pi.dk/3 || curl pi.dk/3/ || fetch -o - http://pi.dk/3) | bash 

Se descargará, verificará la firma y realizará una instalación personal si no se puede instalar de manera global.

Mira los videos introductorios de GNU Parallel para obtener más información: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

El siguiente script muestra una forma de hacer esto con las funciones. Puede colocar las funciones bgxupdate y bgxlimit en su secuencia de comandos o tenerlas en un archivo separado que se obtiene de su secuencia de comandos con:

 . /path/to/bgx.sh 

Tiene la ventaja de que puede mantener múltiples grupos de procesos de forma independiente (puede ejecutar, por ejemplo, un grupo con un límite de 10 y otro grupo totalmente separado con un límite de 3).

Utilizó los jobs incorporados de bash para obtener una lista de subprocesos, pero los mantiene en variables individuales. En el bucle en la parte inferior, puede ver cómo llamar a la función bgxlimit :

  • configurar una variable de grupo vacía.
  • transferir eso a bgxgrp .
  • llama a bgxlimit con el límite y el comando que deseas ejecutar.
  • transfiere el nuevo grupo a tu variable de grupo.

Por supuesto, si solo tiene un grupo, simplemente use bgxgrp directamente en lugar de transferirlo hacia adentro y hacia afuera.

 #!/bin/bash # bgxupdate - update active processes in a group. # Works by transferring each process to new group # if it is still active. # in: bgxgrp - current group of processes. # out: bgxgrp - new group of processes. # out: bgxcount - number of processes in new group. bgxupdate() { bgxoldgrp=${bgxgrp} bgxgrp="" ((bgxcount = 0)) bgxjobs=" $(jobs -pr | tr '\n' ' ')" for bgxpid in ${bgxoldgrp} ; do echo "${bgxjobs}" | grep " ${bgxpid} " >/dev/null 2>&1 if [[ $? -eq 0 ]] ; then bgxgrp="${bgxgrp} ${bgxpid}" ((bgxcount = bgxcount + 1)) fi done } # bgxlimit - start a sub-process with a limit. # Loops, calling bgxupdate until there is a free # slot to run another sub-process. Then runs it # an updates the process group. # in: $1 - the limit on processes. # in: $2+ - the command to run for new process. # in: bgxgrp - the current group of processes. # out: bgxgrp - new group of processes bgxlimit() { bgxmax=$1 ; shift bgxupdate while [[ ${bgxcount} -ge ${bgxmax} ]] ; do sleep 1 bgxupdate done if [[ "$1" != "-" ]] ; then $* & bgxgrp="${bgxgrp} $!" fi } # Test program, create group and run 6 sleeps with # limit of 3. group1="" echo 0 $(date | awk '{print $4}') '[' ${group1} ']' echo for i in 1 2 3 4 5 6 ; do bgxgrp=${group1} ; bgxlimit 3 sleep ${i}0 ; group1=${bgxgrp} echo ${i} $(date | awk '{print $4}') '[' ${group1} ']' done # Wait until all others are finished. echo bgxgrp=${group1} ; bgxupdate ; group1=${bgxgrp} while [[ ${bgxcount} -ne 0 ]] ; do oldcount=${bgxcount} while [[ ${oldcount} -eq ${bgxcount} ]] ; do sleep 1 bgxgrp=${group1} ; bgxupdate ; group1=${bgxgrp} done echo 9 $(date | awk '{print $4}') '[' ${group1} ']' done 

Aquí hay una muestra de ejecución:

 0 12:38:00 [ ] 1 12:38:00 [ 3368 ] 2 12:38:00 [ 3368 5880 ] 3 12:38:00 [ 3368 5880 2524 ] 4 12:38:10 [ 5880 2524 1560 ] 5 12:38:20 [ 2524 1560 5032 ] 6 12:38:30 [ 1560 5032 5212 ] 9 12:38:50 [ 5032 5212 ] 9 12:39:10 [ 5212 ] 9 12:39:30 [ ] 
  • Todo comienza a las 12:38:00 y, como puede ver, los primeros tres procesos se ejecutan inmediatamente.
  • Cada proceso duerme durante n*10 segundos, por lo que el cuarto proceso no comienza hasta que sale el primero (en el momento t = 10 o 12:38:10). Puede ver que el proceso 3368 ha desaparecido de la lista antes de agregar 1560.
  • De forma similar, el quinto proceso (5032) comienza cuando el segundo (5880) sale en el momento t = 20.
  • Y finalmente, el sexto proceso (5212) comienza cuando el tercero (2524) sale en el momento t = 30.
  • Luego comienza el resumen, el cuarto proceso sale en t = 50 (comenzó en 10, duración de 40), quinto en t = 70 (comenzó en 20, duración de 50) y sexto en t = 90 (comenzó en 30, duración de 60 )

O bien, en forma de línea de tiempo:

 Process: 1 2 3 4 5 6 -------- - - - - - - 12:38:00 ^ ^ ^ 12:38:10 v | | ^ 12:38:20 v | | ^ 12:38:30 v | | ^ 12:38:40 | | | 12:38:50 v | | 12:39:00 | | 12:39:10 v | 12:39:20 | 12:39:30 v 

Un pequeño script bash podría ayudarte:

 # content of script exec-async.sh joblist=($(jobs -p)) while (( ${#joblist[*]} >= 3 )) do sleep 1 joblist=($(jobs -p)) done $* & 

Si llamas:

 . exec-async.sh sleep 10 

… cuatro veces, las primeras tres llamadas volverán inmediatamente, la cuarta llamada se bloqueará hasta que se ejecuten menos de tres trabajos.

Debe iniciar este script dentro de la sesión actual con el prefijo . , porque jobs enumera solo los trabajos de la sesión actual.

El sleep dentro es feo, pero no encontré la manera de esperar al primer trabajo que finaliza.

Aquí está la manera más corta:

 waitforjobs() { while test $(jobs -p | wc -w) -ge "$1"; do wait -n; done } 

Llame a esta función antes de bifurcar cualquier nuevo trabajo:

 waitforjobs 10 run_another_job & 

Para tener tantos trabajos de fondo como núcleos en la máquina, use $(nproc) lugar de un número fijo como 10.

Suponiendo que le gustaría escribir un código como este:

 for x in $(seq 1 100); do # 100 things we want to put into the background. max_bg_procs 5 # Define the limit. See below. your_intensive_job & done 

Donde max_bg_procs debe colocarse en su .bashrc :

 function max_bg_procs { if [[ $# -eq 0 ]] ; then echo "Usage: max_bg_procs NUM_PROCS. Will wait until the number of background (&)" echo " bash processes (as determined by 'jobs -pr') falls below NUM_PROCS" return fi local max_number=$((0 + ${1:-0})) while true; do local current_number=$(jobs -pr | wc -l) if [[ $current_number -lt $max_number ]]; then break fi sleep 1 done } 

Esto podría ser lo suficientemente bueno para la mayoría de los propósitos, pero no es óptimo.

 #!/bin/bash n=0 maxjobs=10 for i in *.m4a ; do # ( DO SOMETHING ) & # limit jobs if (( $(($((++n)) % $maxjobs)) == 0 )) ; then wait # wait until all have finished (not optimal, but most times good enough) echo $n wait fi done 

Si estás dispuesto a hacer esto fuera de pure bash, deberías buscar un sistema de cola de trabajos.

Por ejemplo, hay cola GNU o PBS . Y para PBS, es posible que desee buscar en Maui para la configuración.

Ambos sistemas requerirán cierta configuración, pero es completamente posible permitir que se ejecute una cantidad específica de trabajos al mismo tiempo, solo comenzando trabajos recién en cola cuando finaliza un trabajo en ejecución. Típicamente, estos sistemas de colas de trabajos se usarían en clústeres de supercomputación, donde le gustaría asignar una cantidad específica de memoria o tiempo de computación a cualquier trabajo por lotes dado; sin embargo, no hay ninguna razón por la que no pueda usar uno de estos en una sola computadora de escritorio sin tener en cuenta el tiempo de cómputo o los límites de memoria.

En Linux utilizo esto para limitar los trabajos de bash a la cantidad de CPU disponibles (posiblemente CPU_NUMBER al configurar la CPU_NUMBER ).

 [ "$CPU_NUMBER" ] || CPU_NUMBER="`nproc 2>/dev/null || echo 1`" while [ "$1" ]; do { do something with $1 in parallel echo "[$# items left] $1 done" } & while true; do # load the PIDs of all child processes to the array joblist=(`jobs -p`) if [ ${#joblist[*]} -ge "$CPU_NUMBER" ]; then # when the job limit is reached, wait for *single* job to finish wait -n else # stop checking when we're below the limit break fi done # it's great we executed zero external commands to check! shift done # wait for all currently active child processes wait 

La siguiente función (desarrollada a partir de la respuesta de tangens anterior, ya sea copiar en el script o fuente del archivo):

 job_limit () { # Test for single positive integer input if (( $# == 1 )) && [[ $1 =~ ^[1-9][0-9]*$ ]] then # Check number of running jobs joblist=($(jobs -rp)) while (( ${#joblist[*]} >= $1 )) do # Wait for any job to finish command='wait '${joblist[0]} for job in ${joblist[@]:1} do command+=' || wait '$job done eval $command joblist=($(jobs -rp)) done fi } 

1) Solo requiere insertar una sola línea para limitar un bucle existente

 while : do task & job_limit `nproc` done 

2) Espera a la finalización de las tareas en segundo plano existentes en lugar de las encuestas, lo que aumenta la eficiencia para tareas rápidas

¿Ha considerado comenzar diez procesos de escucha de larga duración y comunicarse con ellos a través de canalizaciones con nombre?

puedes usar ulimit -u mira http://ss64.com/bash/ulimit.html