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

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

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

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

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

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

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

Как бы вы написали этот цикл, чтобы всегда были Xdo-somethingработает сразу?

 Tuttle10 мар. 2015 г., 15:17
Я бы порекомендовал мое решениеstackoverflow.com/a/28965927/340581
 unwind19 мая 2009 г., 09:54
Как sidenode, я мечтал добавить опцию make -j в bash для примитива. Это не будет работать всегда, но для некоторых простых случаев, когда вы знаете, что тело цикла будет делать что-то уникальное для каждой итерации, было бы довольно просто сказать «для -j 4 ...».
 paxdiablo06 нояб. 2009 г., 14:53
Перекрестная ссылка наstackoverflow.com/questions/1537956/... для решения bash, которое уменьшает проблемы с производительностью и учитывает группы подпроцессов, хранящихся отдельно.

Ответы на вопрос(16)

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

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

сделанный

Ndomains =echo $DOMAINS |wc -w

для i in $ (seq 1 1 $ Ndomains) сделайте эхо "wait for $ {job [$ i]}" wait "$ {job [$ i]}" готово

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

С GNU Parallelhttp://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

 tripleee25 июл. 2016 г., 06:51
Плюс 1 хотяcat это, конечно,бесполезный.
 Leo Izen01 дек. 2013 г., 21:50
Обратите внимание, что это действительно полезно при использованииfind Команда для создания списка файлов, потому что это не только предотвращает проблему, когда есть пробел внутри имени файла, который возникает вfor i in ...; do но найти тоже можно сделатьfind -name \*.extension1 -or -name \*.extension2 с какой параллелью GNU {.} можно справиться очень хорошо.
 Ole Tange25 июл. 2016 г., 10:21
@tripleee Re: Бесполезное использование кота. Увидетьoletange.blogspot.dk/2013/10/useless-use-of-cat.html
 tripleee25 июл. 2016 г., 10:28
Это Ты! Кстати, не могли бы вы обновить ссылку в этом блоге? К сожалению, местоположение partmaps.org мертво, но перенаправитель Iki должен продолжать работать.

отслеживать ошибки и обрабатывать уязвимые / зомби-процессы:

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"

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

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

Свдохновение для обработки мертвых / зомбиВот чистое решение Bash:

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

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

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

е целые числа на 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 равным количеству процессоров, которые у вас есть.

Вот как мне удалось решить эту проблему в 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

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

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

Вместо простого 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 Вы просто проверяете, что содержит переменная.

 gerikson16 сент. 2008 г., 20:47
Спасибо за это, хотя код не закончен, он дал мне ответ на проблему, с которой я столкнулся на работе.
 Girardi03 мая 2018 г., 17:18
единственная проблема в том, что если вы убьете скрипт переднего плана (тот, что с циклом), то запущенные задания не будут уничтожены вместе

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.

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

INPUT  = a.input b.input
OUTPUT = $(INPUT:.input=.output)

%.output : %.input
    $(SOME_COMMAND) $< [email protected]

all: $(OUTPUT)

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

make -j<NUMBER>

выполнять не более NUMBER команд параллельно.

Вот альтернативное решение, которое можно вставить в .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, но не представляется очевидным решение подождать, пока не завершится какое-либо из заданий вместо всех.

Решение Вопроса

что вы хотите сделать, 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.
 mr.spuratic03 дек. 2013 г., 19:21
cpus=$(getconf _NPROCESSORS_ONLN)
 amphetamachine22 мар. 2010 г., 03:31
Этот метод, на мой взгляд, является самым элегантным решением. За исключением того, что я параноик, я всегда люблю использоватьfind [...] -print0 а такжеxargs -0.
 EverythingRightPlace13 мар. 2015 г., 13:18
Из руководства, почему бы не использовать--max-procs=0 чтобы получить как можно больше процессов?
 Toby Speight26 февр. 2016 г., 11:30
@EverythingRightPlace, вопрос явно требует не больше процессов, чем доступные процессоры.--max-procs=0 больше похоже на попытку спрашивающего (запустить столько процессов, сколько аргументов).

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

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

с помощью:

cat my_commands | parallel -j 4
 tripleee25 июл. 2016 г., 06:53
Использованиеmake -j это умно, но без объяснения и этого блога кода Awk только для записи, я воздерживаюсь от голосования.
maxjobs=4
parallelize () {
        while [ $# -gt 0 ] ; do
                jobcnt=(`jobs -p`)
                if [ ${#jobcnt[@]} -lt $maxjobs ] ; then
                        do-something $1 &
                        shift  
                else
                        sleep 1
                fi
        done
        wait
}

parallelize arg1 arg2 "5 args to third job" arg4 ...
 lhunath19 мая 2009 г., 08:32
Поймите, что есть некоторыесерьезный здесь происходит занижение, поэтому любые задания, требующие пробелов в аргументах, потерпят неудачу; более того, этот сценарий поглотит ваш процессор, пока он ожидает завершения некоторых заданий, если запрошено больше заданий, чем позволяет maxjobs.
 amphetamachine22 мар. 2010 г., 02:10
Возможно, вы захотите использовать «jobs -pr», чтобы ограничить выполнение заданий.
 lhunath19 мая 2009 г., 09:01
Также обратите внимание, что это предполагает, что ваш скрипт не делает ничего другого с заданиями; если вы это сделаете, то они тоже будут считаться с maxjobs.
 euphoria8329 авг. 2016 г., 07:26
Добавлена ​​команда sleep, чтобы не допустить повторения цикла while без перерыва, пока он ожидает завершения уже запущенных команд do-что-либо. В противном случае этот цикл по существу будет занимать одно из ядер ЦП. Это также решает проблему @lhunath.

Ваш ответ на вопрос