Распараллелить скрипт Bash с максимальным количеством процессов

86

Допустим, у меня есть цикл в Bash:

for foo in `some-command`
do
   do-something $foo
done

do-somethingпривязан к процессору, и у меня есть красивый 4-ядерный процессор. Я хотел бы иметь возможность запускать до 4 do-something-х одновременно.

Наивный подход выглядит так:

for foo in `some-command`
do
   do-something $foo &
done

Это будет работать все do-something с на один раз, но есть пара минусов, в основном , что делать-то , возможно , также имеет некоторый существенный ввод / вывод , которые исполняющее все сразу может замедлить немного. Другая проблема заключается в том, что этот блок кода возвращается немедленно, поэтому нет возможности выполнять другую работу, когда все do-somethings завершены.

Как бы вы написали этот цикл, чтобы X всегда do-somethingвыполнялись одновременно?

thelsdj
источник
2
В качестве побочного элемента я мечтал добавить опцию make -j в bash для примитивов. Это не сработает всегда, но для некоторых простых случаев, когда вы знаете, что тело цикла будет делать что-то уникальное для каждой итерации, было бы довольно просто сказать «for -j 4 ...».
расслабьтесь
1
Перекрестная ссылка на stackoverflow.com/questions/1537956/… для решения bash, которое смягчает проблемы с производительностью и позволяет хранить отдельные группы подпроцессов.
paxdiablo
1
Я бы порекомендовал свое решение stackoverflow.com/a/28965927/340581
Tuttle

Ответы:

62

В зависимости от того, что вы хотите сделать, xargs также может помочь (здесь: преобразование документов с помощью pdf2ps):

cpus=$( ls -d /sys/devices/system/cpu/cpu[[:digit:]]* | wc -w )

find . -name \*.pdf | xargs --max-args=1 --max-procs=$cpus  pdf2ps

Из документов:

--max-procs=max-procs
-P max-procs
       Run up to max-procs processes at a time; the default is 1.
       If max-procs is 0, xargs will run as many processes as  possible  at  a
       time.  Use the -n option with -P; otherwise chances are that only one
       exec will be done.
Фриц Г. Менер
источник
9
Этот способ, на мой взгляд, наиболее элегантное решение. Кроме того, поскольку я параноик, я всегда люблю использовать find [...] -print0и xargs -0.
amphetamachine
7
cpus=$(getconf _NPROCESSORS_ONLN)
mr.spuratic 03
1
Из руководства, почему бы не использовать, --max-procs=0чтобы получить как можно больше процессов?
EverythingRightPlace
@EverythingRightPlace, вопрос явно требует не больше процессов, чем доступных процессоров. --max-procs=0больше похоже на попытку вопрошающего (запустить столько процессов, сколько аргументов).
Тоби Спейт,
39

С GNU Parallel http://www.gnu.org/software/parallel/ вы можете написать:

some-command | parallel do-something

GNU Parallel также поддерживает выполнение заданий на удаленных компьютерах. Это будет работать по одному на ядро ​​ЦП на удаленных компьютерах, даже если у них разное количество ядер:

some-command | parallel -S server1,server2 do-something

Более сложный пример: здесь мы перечисляем файлы, на которых мы хотим запустить my_script. Файлы имеют расширение (может быть .jpeg). Мы хотим, чтобы вывод my_script располагался рядом с файлами в basename.out (например, foo.jpeg -> foo.out). Мы хотим запустить my_script один раз для каждого ядра компьютера, и мы хотим запустить его также на локальном компьютере. Для удаленных компьютеров мы хотим, чтобы файл был обработан и перенесен на данный компьютер. Когда my_script завершится, мы хотим, чтобы foo.out был передан обратно, а затем мы хотим, чтобы foo.jpeg и foo.out были удалены с удаленного компьютера:

cat list_of_files | \
parallel --trc {.}.out -S server1,server2,: \
"my_script {} > {.}.out"

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

some-command | parallel do-something | postprocess

Дополнительные примеры смотрите в видео: https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1

Оле Танге
источник
1
Обратите внимание, что это действительно полезно при использовании findкоманды для генерации списка файлов, потому что это не только предотвращает проблему, когда внутри имени файла есть пробел, for i in ...; doно и find может также сделать то, с find -name \*.extension1 -or -name \*.extension2чем GNU parallel {.} Может очень хорошо справиться.
Лео Изен
Плюс 1 хоть и бесполезенcat , конечно .
Tripleee
@tripleee Re: Бесполезное использование cat. См. Oletange.blogspot.dk/2013/10/useless-use-of-cat.html
Ole Tange
Это Ты! Кстати, не могли бы вы обновить ссылку в том блоге? Местоположение partmaps.org, к сожалению, мертво, но перенаправитель Iki должен продолжать работать.
Tripleee
22
maxjobs = 4
parallelize () {
        а [$ # -gt 0]; делать
                jobcnt = (`вакансии -p`)
                если [$ {# jobcnt [@]} -lt $ maxjobs]; тогда
                        сделать что-нибудь $ 1 и
                        сдвиг  
                еще
                        спать 1
                фи
        сделанный
        Подождите
}

распараллелить arg1 arg2 "5 аргументов третьему заданию" arg4 ...
Bstark
источник
10
Поймите, что здесь происходит серьезное недооценка, поэтому любые задания, требующие пробелов в аргументах, потерпят неудачу; более того, этот скрипт заживо съест ваш процессор, ожидая завершения некоторых заданий, если запрашивается больше заданий, чем позволяет maxjobs.
lhunath
1
Также обратите внимание, что это предполагает, что ваш скрипт не делает ничего другого, что связано с заданиями; если да, то они также засчитываются в счет макс.
lhunath
1
Вы можете использовать "jobs -pr", чтобы ограничить выполнение заданий.
amphetamachine
1
Добавлена ​​команда сна для предотвращения повторения цикла while без перерыва, пока он ожидает завершения уже запущенных команд do-something. В противном случае этот цикл фактически занял бы одно из ядер ЦП. Это также решает проблему @lhunath.
euphoria83
12

Вот альтернативное решение, которое можно вставить в .bashrc и использовать для повседневного использования одного лайнера:

function pwait() {
    while [ $(jobs -p | wc -l) -ge $1 ]; do
        sleep 1
    done
}

Чтобы использовать его, все, что нужно сделать, это поставить &после заданий и вызова pwait, параметр дает количество параллельных процессов:

for i in *; do
    do_something $i &
    pwait 10
done

Было бы лучше использовать waitвместо занятого ожидания вывода jobs -p, но, похоже, не существует очевидного решения - дождаться завершения любого из заданных заданий вместо их всех.

Грумбель
источник
11

Вместо простого bash используйте Makefile, а затем укажите количество одновременных заданий, make -jXгде X - количество заданий, запускаемых одновременно.

Или вы можете использовать wait(" man wait"): запустить несколько дочерних процессов, вызвать wait- он завершится, когда дочерние процессы закончатся.

maxjobs = 10

foreach line in `cat file.txt` {
 jobsrunning = 0
 while jobsrunning < maxjobs {
  do job &
  jobsrunning += 1
 }
wait
}

job ( ){
...
}

Если вам нужно сохранить результат задания, присвойте его результат переменной. После waitтого, как вы просто проверите, что содержит переменная.

сколима
источник
1
Спасибо за это, хотя код не закончен, он дал мне ответ на проблему, с которой я столкнулся на работе.
gerikson
единственная проблема в том, что если вы убьете скрипт переднего плана (тот, у кого есть цикл), то выполнявшиеся задания не будут убиты вместе
Жирарди
8

Может быть, вместо переписывания цикла попробовать утилиту распараллеливания? Я большой поклонник xjobs. Я постоянно использую xjobs для массового копирования файлов по нашей сети, обычно при настройке нового сервера базы данных. http://www.maier-komor.de/xjobs.html

тессеин
источник
7

Если вы знакомы с этой makeкомандой, в большинстве случаев вы можете выразить список команд, которые хотите запустить, в виде файла makefile. Например, если вам нужно запустить $ SOME_COMMAND для файлов * .input, каждый из которых создает * .output, вы можете использовать make-файл

INPUT = a.input b.input
ВЫХОД = $ (ВХОД:. Ввод =. Вывод)

%.выход Вход
    $ (НЕКОТОРЫЕ_КОМАНДА) $ <$ @

все: $ (ВЫХОД)

а потом просто беги

make -j <НОМЕР>

для параллельного выполнения не более ЧИСЛА команд.

Idelic
источник
6

Хотя сделать это правильно, bashвероятно, невозможно, вы можете довольно легко сделать полуправо. bstarkдал хорошее приближение к праву, но у него есть следующие недостатки:

  • Разделение слов: вы не можете передавать ему задания, в аргументах которых используются следующие символы: пробелы, табуляции, новые строки, звездочки, вопросительные знаки. Если вы это сделаете, все сломается, возможно, неожиданно.
  • Он полагается на то, что остальная часть вашего скрипта ничего не создает в фоновом режиме. Если вы это сделаете или позже вы добавите что-то в сценарий, который будет отправлен в фоновом режиме, потому что вы забыли, что вам не разрешено использовать фоновые задания из-за его фрагмента, все сломается.

Еще одно приближение, не имеющее этих недостатков:

scheduleAll() {
    local job i=0 max=4 pids=()

    for job; do
        (( ++i % max == 0 )) && {
            wait "${pids[@]}"
            pids=()
        }

        bash -c "$job" & pids+=("$!")
    done

    wait "${pids[@]}"
}

Обратите внимание, что этот легко адаптируется, чтобы также проверять код выхода каждого задания по мере его завершения, поэтому вы можете предупреждать пользователя, если задание не удается, или установить код выхода в scheduleAllсоответствии с количеством заданий, которые не удалось выполнить, или что-то еще.

Проблема с этим кодом как раз в том, что:

  • Он планирует четыре (в данном случае) задания одновременно, а затем ожидает завершения всех четырех. Некоторые из них могут быть выполнены раньше, чем другие, что приведет к тому, что следующий пакет из четырех заданий будет ждать, пока не будет выполнено самое длинное из предыдущего пакета.

Решение, которое решает эту последнюю проблему, должно использовать kill -0 для опроса, исчезли ли какие-либо процессы вместоwait запланировать следующее задание. Однако это создает небольшую новую проблему: у вас есть состояние гонки между завершением задания и kill -0проверкой того, закончилось ли оно. Если задание завершилось и в то же время запускается другой процесс в вашей системе, принимая случайный PID, который является идентификатором только что завершенного задания, kill -0он не заметит, что ваша работа завершена, и все снова сломается.

Идеальное решение невозможно в bash.

лхунатх
источник
3

функция для bash:

parallel ()
{
    awk "BEGIN{print \"all: ALL_TARGETS\\n\"}{print \"TARGET_\"NR\":\\n\\t@-\"\$0\"\\n\"}END{printf \"ALL_TARGETS:\";for(i=1;i<=NR;i++){printf \" TARGET_%d\",i};print\"\\n\"}" | make $@ -f - all
}

с помощью:

cat my_commands | parallel -j 4
ильнар
источник
Использование make -jявляется умным, но без объяснения причин и с этим куском кода Awk только для записи, я воздерживаюсь от голосования за.
Tripleee
2

В проекте, над которым я работаю, используется команда ожидания для управления процессами параллельной оболочки (на самом деле ksh). Чтобы решить ваши проблемы с вводом-выводом, в современной ОС, возможно, параллельное выполнение действительно повысит эффективность. Если все процессы читают одни и те же блоки на диске, только первый процесс должен будет воздействовать на физическое оборудование. Другие процессы часто могут получить блок из дискового кеша ОС в памяти. Очевидно, что чтение из памяти на несколько порядков быстрее, чем чтение с диска. Кроме того, это преимущество не требует изменения кода.

Джон Эриксон
источник
1

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

#!/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
кошка
источник
1

Вот как мне удалось решить эту проблему в сценарии bash:

 #! /bin/bash

 MAX_JOBS=32

 FILE_LIST=($(cat ${1}))

 echo Length ${#FILE_LIST[@]}

 for ((INDEX=0; INDEX < ${#FILE_LIST[@]}; INDEX=$((${INDEX}+${MAX_JOBS})) ));
 do
     JOBS_RUNNING=0
     while ((JOBS_RUNNING < MAX_JOBS))
     do
         I=$((${INDEX}+${JOBS_RUNNING}))
         FILE=${FILE_LIST[${I}]}
         if [ "$FILE" != "" ];then
             echo $JOBS_RUNNING $FILE
             ./M22Checker ${FILE} &
         else
             echo $JOBS_RUNNING NULL &
         fi
         JOBS_RUNNING=$((JOBS_RUNNING+1))
     done
     wait
 done
Фернандо
источник
1

В самом деле поздно на вечеринку здесь, но вот другое решение.

Многие решения не обрабатывают пробелы / специальные символы в командах, не поддерживают выполнение N заданий постоянно, используют процессор в циклах занятости или полагаются на внешние зависимости (например, GNU parallel ).

С вдохновением для обработки процесса мертвых / зомби , вот чистый раствор Баша:

function run_parallel_jobs {
    local concurrent_max=$1
    local callback=$2
    local cmds=("${@:3}")
    local jobs=( )

    while [[ "${#cmds[@]}" -gt 0 ]] || [[ "${#jobs[@]}" -gt 0 ]]; do
        while [[ "${#jobs[@]}" -lt $concurrent_max ]] && [[ "${#cmds[@]}" -gt 0 ]]; do
            local cmd="${cmds[0]}"
            cmds=("${cmds[@]:1}")

            bash -c "$cmd" &
            jobs+=($!)
        done

        local job="${jobs[0]}"
        jobs=("${jobs[@]:1}")

        local state="$(ps -p $job -o state= 2>/dev/null)"

        if [[ "$state" == "D" ]] || [[ "$state" == "Z" ]]; then
            $callback $job
        else
            wait $job
            $callback $job $?
        fi
    done
}

И пример использования:

function job_done {
    if [[ $# -lt 2 ]]; then
        echo "PID $1 died unexpectedly"
    else
        echo "PID $1 exited $2"
    fi
}

cmds=( \
    "echo 1; sleep 1; exit 1" \
    "echo 2; sleep 2; exit 2" \
    "echo 3; sleep 3; exit 3" \
    "echo 4; sleep 4; exit 4" \
    "echo 5; sleep 5; exit 5" \
)

# cpus="$(getconf _NPROCESSORS_ONLN)"
cpus=3
run_parallel_jobs $cpus "job_done" "${cmds[@]}"

Выход:

1
2
3
PID 56712 exited 1
4
PID 56713 exited 2
5
PID 56714 exited 3
PID 56720 exited 4
PID 56724 exited 5

Для обработки вывода для каждого процесса $$может использоваться запись в файл, например:

function job_done {
    cat "$1.log"
}

cmds=( \
    "echo 1 \$\$ >\$\$.log" \
    "echo 2 \$\$ >\$\$.log" \
)

run_parallel_jobs 2 "job_done" "${cmds[@]}"

Выход:

1 56871
2 56872
Скрат
источник
0

Вы можете использовать простой вложенный цикл for (замените N и M соответствующими целыми числами ниже):

for i in {1..N}; do
  (for j in {1..M}; do do_something; done & );
done

Это выполнит do_something N * M раз в M раундах, каждый раунд выполняет N заданий параллельно. Вы можете сделать N равным количеству имеющихся у вас процессоров.

Адам Зальцман
источник
0

Мое решение - всегда поддерживать заданное количество процессов, отслеживать ошибки и обрабатывать непрерывные / зомби-процессы:

function log {
    echo "$1"
}

# Take a list of commands to run, runs them sequentially with numberOfProcesses commands simultaneously runs
# Returns the number of non zero exit codes from commands
function ParallelExec {
    local numberOfProcesses="${1}" # Number of simultaneous commands to run
    local commandsArg="${2}" # Semi-colon separated list of commands

    local pid
    local runningPids=0
    local counter=0
    local commandsArray
    local pidsArray
    local newPidsArray
    local retval
    local retvalAll=0
    local pidState
    local commandsArrayPid

    IFS=';' read -r -a commandsArray <<< "$commandsArg"

    log "Runnning ${#commandsArray[@]} commands in $numberOfProcesses simultaneous processes."

    while [ $counter -lt "${#commandsArray[@]}" ] || [ ${#pidsArray[@]} -gt 0 ]; do

        while [ $counter -lt "${#commandsArray[@]}" ] && [ ${#pidsArray[@]} -lt $numberOfProcesses ]; do
            log "Running command [${commandsArray[$counter]}]."
            eval "${commandsArray[$counter]}" &
            pid=$!
            pidsArray+=($pid)
            commandsArrayPid[$pid]="${commandsArray[$counter]}"
            counter=$((counter+1))
        done


        newPidsArray=()
        for pid in "${pidsArray[@]}"; do
            # Handle uninterruptible sleep state or zombies by ommiting them from running process array (How to kill that is already dead ? :)
            if kill -0 $pid > /dev/null 2>&1; then
                pidState=$(ps -p$pid -o state= 2 > /dev/null)
                if [ "$pidState" != "D" ] && [ "$pidState" != "Z" ]; then
                    newPidsArray+=($pid)
                fi
            else
                # pid is dead, get it's exit code from wait command
                wait $pid
                retval=$?
                if [ $retval -ne 0 ]; then
                    log "Command [${commandsArrayPid[$pid]}] failed with exit code [$retval]."
                    retvalAll=$((retvalAll+1))
                fi
            fi
        done
        pidsArray=("${newPidsArray[@]}")

        # Add a trivial sleep time so bash won't eat all CPU
        sleep .05
    done

    return $retvalAll
}

Применение:

cmds="du -csh /var;du -csh /tmp;sleep 3;du -csh /root;sleep 10; du -csh /home"

# Execute 2 processes at a time
ParallelExec 2 "$cmds"

# Execute 4 processes at a time
ParallelExec 4 "$cmds"
Орсирис де Йонг
источник
-1

$ DOMAINS = "список некоторых доменов в командах" для foo в some-command do

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

сделанный

Ndomains =echo $DOMAINS |wc -w

для i в $ (seq 1 1 $ Ndomains) do echo "ждать $ {job [$ i]}" ждать "$ {job [$ i]}" выполнено

в этой концепции будет работать для распараллеливания. Важно то, что последняя строка eval - "&", которая помещает команды в фон.

Джек
источник