Bash: ограничить количество одновременных заданий? - PullRequest
26 голосов
/ 08 октября 2009

Есть ли простой способ ограничить количество одновременных заданий в bash? Под этим я подразумеваю создание блока &, если в фоновом режиме выполняется более n параллельных заданий.

Я знаю, что могу реализовать это с помощью ps | трюки в стиле grep, но есть ли более простой способ?

Ответы [ 12 ]

19 голосов
/ 06 ноября 2009

Следующий скрипт показывает способ сделать это с помощью функций. Вы можете поместить функции bgxupdate и bgxlimit в свой сценарий или поместить их в отдельный файл, полученный из сценария с помощью:

. /path/to/bgx.sh

Преимущество состоит в том, что вы можете поддерживать несколько групп процессов независимо (вы можете запустить, например, одну группу с пределом 10 и другую полностью отдельную группу с пределом 3).

Он использовал встроенный bash, jobs, чтобы получить список подпроцессов, но поддерживает их в отдельных переменных. В нижнем цикле вы можете увидеть, как вызывать функцию bgxlimit:

  • установить пустую групповую переменную.
  • передать это bgxgrp.
  • вызовите bgxlimit с лимитом и командой, которую вы хотите выполнить.
  • перенести новую группу обратно в переменную группы.

Конечно, если у вас есть только одна группа, просто используйте bgxgrp напрямую, а не входите и выходите.

#!/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

Вот пример прогона:

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 [ ]
  • Все начинается в 12:38:00, и, как вы можете видеть, первые три процесса запускаются немедленно.
  • Каждый процесс спит в течение n*10 секунд, поэтому четвертый процесс не запускается, пока не завершится первый (в момент времени t = 10 или 12:38:10). Вы можете видеть, что процесс 3368 исчез из списка до добавления 1560.
  • Аналогично, пятый процесс (5032) начинается, когда второй (5880) выходит в момент времени t = 20.
  • И, наконец, шестой процесс (5212) начинается, когда третий (2524) выходит в момент времени t = 30.
  • Затем начинается краткое изложение, четвертый процесс завершается при t = 50 (начало в 10, продолжительность 40), пятый в t = 70 (начало в 20, продолжительность 50) и шестой в t = 90 (начинается в 30, продолжительность 60).

Или в форме временной шкалы:

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
19 голосов
/ 19 мая 2012

Если у вас установлен GNU Parallel http://www.gnu.org/software/parallel/, вы можете сделать это:

parallel gzip ::: *.log

, который будет запускать по одному gzip на ядро ​​ЦП, пока не будут сжаты все файлы журналов.

Если это часть большего цикла, вы можете использовать sem вместо:

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

Он будет делать то же самое, но даст вам возможность делать больше вещей для каждого файла.

Если GNU Parallel не упакован для вашего дистрибутива, вы можете установить GNU Parallel просто:

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

Он загрузит, проверит подпись и выполнит личную установку, если не сможет выполнить глобальную установку.

Посмотрите вступительные видеоролики по GNU Parallel, чтобы узнать больше: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

19 голосов
/ 08 октября 2009

Небольшой скрипт bash может помочь вам:

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

Если вы позвоните:

. exec-async.sh sleep 10

... четыре раза, первые три вызова вернутся немедленно, четвертый вызов будет заблокирован, пока не будет запущено менее трех заданий.

Вам необходимо запустить этот сценарий внутри текущего сеанса, добавив к нему префикс ., поскольку в jobs перечислены только задания текущего сеанса.

Внутри sleep уродливо, но я не нашел способа дождаться первой работы, которая заканчивается.

13 голосов
/ 22 мая 2015

Вот кратчайший путь:

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

Вызовите эту функцию перед тем, как отказаться от любого нового задания:

waitforjobs 10
run_another_job &

Чтобы иметь столько фоновых заданий, сколько ядер на машине, используйте $(nproc) вместо фиксированного числа, например 10.

10 голосов
/ 15 сентября 2012

Предполагая, что вы хотите написать код, подобный этому:

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

Где max_bg_procs должно быть указано в вашем .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
}
5 голосов
/ 21 июля 2011

Это может быть достаточно для большинства целей, но не оптимально.

#!/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
3 голосов
/ 10 октября 2015

Следующая функция (разработанная на основе тангенса, ответ выше, либо копирование в скрипт, либо источник из файла):

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) Требуется только вставить одну строку, чтобы ограничить существующий цикл

while :
do
    task &
    job_limit `nproc`
done

2) Ожидает завершения существующих фоновых задач, а не опроса, повышая эффективность быстрых задач

3 голосов
/ 05 ноября 2009

Если вы хотите сделать это за пределами чистого Bash, вы должны изучить систему очередей на работу.

Например, есть очередь GNU или PBS . А для PBS вы можете посмотреть Maui для конфигурации.

Обе системы потребуют некоторой конфигурации, но вполне возможно разрешить одновременное выполнение определенного количества заданий, только начиная новые задания в очереди, когда завершенное задание завершается. Как правило, эти системы очередей заданий используются в суперкомпьютерных кластерах, где вы хотите выделить определенный объем памяти или вычислительное время для любого заданного пакетного задания; тем не менее, нет причин, по которым вы не можете использовать один из них на одном настольном компьютере без учета времени вычислений или памяти.

1 голос
/ 10 марта 2015

В Linux я использую это, чтобы ограничить число заданий bash количеством доступных процессоров (возможно, переопределить, задав 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
0 голосов
/ 22 июня 2019

Трудно обойтись без wait -n (например, оболочка в busybox не поддерживает его). Так что здесь есть обходной путь, он не оптимален, потому что он вызывает команды 'jobs' и 'wc' 10 раз в секунду Например, вы можете уменьшить количество звонков до 1x в секунду, если не возражаете подождать немного дольше для завершения каждой работы.

# $1 = maximum concurent jobs
#
limit_jobs()
{
   while true; do
      if [ "$(jobs -p | wc -l)" -lt "$1" ]; then break; fi
      usleep 100000
   done
}

# and now start some tasks:

task &
limit_jobs 2
task &
limit_jobs 2
task &
limit_jobs 2
task &
limit_jobs 2
wait
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...