Паралелізуйте сценарій 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 s одночасно, але є кілька недоліків, головним чином, що do-something може також мати якийсь значний ввід / вивід, який при виконанні всіх одночасно може трохи сповільнитись. Інша проблема полягає в тому, що цей блок коду негайно повертається, тому жодної можливості робити інші роботи, коли всі do-somethings закінчені.

Як би ви написали цей цикл, щоб завжди do-somethingпрацювали X зразу?


2
Як сиденода, я мріяв додати опцію make--j до bash для примітиву. Це не спрацьовує завжди, але в деяких простих випадках, коли ви знаєте, що тіло циклу буде робити щось унікальне для кожної ітерації, було б досить чисто, просто сказати "для -j 4 ...".
розслабтесь

1
Перехресне посилання на stackoverflow.com/questions/1537956/… для рішення bash, яке пом'якшує проблеми продуктивності та дозволяє створювати окремі групи підпроцесів.
paxdiablo

1
Я б порекомендував своє рішення stackoverflow.com/a/28965927/340581
Таттл

Відповіді:


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.
амфетамахін

7
cpus=$(getconf _NPROCESSORS_ONLN)
mr.spuratic

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 {.} Можуть впоратись дуже добре.
Leo Izen

Плюс 1, хоча catце, звичайно, марно.
триплі

@tripleee Re: Безцільне використання кота. Дивіться oletange.blogspot.dk/2013/10/useless-use-of-cat.html
Ole Tange

О, це ти! Ви, до речі, могли б оновити посилання на цей блог? Місцезнаходження partmaps.org, на жаль, мертве, але редиректор Iki повинен продовжувати працювати.
триплі

22
maxjobs = 4
паралелізувати () {
        в той час як [$ # -gt 0]; робити
                jobcnt = (`Jobs -p`)
                якщо [$ {# jobcnt [@]} -lt $ maxjobs]; тоді
                        зробити щось $ 1 &
                        зміна  
                ще
                        спати 1
                fi
        зроблено
        почекай
}

розпаралелювати arg1 arg2 "5 аргументів до третього завдання" arg4 ...

10
Зрозумійте, що тут відбувається якесь серйозне недооцінювання, тому будь-які роботи, для яких потрібні пробіли в аргументах, зазнають невдачі; більше того, цей скрипт з’їсть ваш процесор живим, поки він чекає закінчення деяких завдань, якщо запитується більше завдань, ніж дозволяє maxjobs.
люнат

1
Також зауважте, що це передбачає, що ваш сценарій не робить нічого іншого, що стосується завдань; якщо ви є, це також враховуватиметься до максджабів.
люнат

1
Можливо, ви захочете використовувати "jobs -pr", щоб обмежитися запущеними завданнями.
амфетамахін

1
Додана команда сну для запобігання повторення циклу while без будь-яких перерв, поки вона чекає завершення вже запущених команд do-something. В іншому випадку цей цикл по суті зайняв би одне з ядер ЦП. Це також стосується занепокоєння @lhunath.
euphoria83

12

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

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

Для його використання потрібно лише поставити &після завдань і виклику в очікуванні параметр задає кількість паралельних процесів:

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командою, більшу частину часу ви можете висловити список команд, які ви хочете запустити як aa make-файл. Наприклад, якщо вам потрібно запустити $ SOME_COMMAND на файлах * .input, кожен з яких видає * .output, ви можете використовувати файл make

INPUT = a.input b.input
ВИХІД = $ (ВХІД: .input = .output)

% .output:% .input
    $ (SOME_COMMAND) $ <$ @

всі: $ (ВИХІД)

а потім просто біжи

зробити -j <НОМЕР>

паралельно запускати не більше NUMBER команд.


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перевіркою, чи закінчилося воно. Якщо робота закінчилася, і одночасно у вашій системі запускається інший процес, беручи випадковий ПІД, який випадково відповідає роботі, яка щойно закінчилась, 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, що має лише запис, я утримуюся від голосування.
триплі

2

Проект, над яким я працюю, використовує команду wait для управління паралельними процесами оболонки (насправді ksh). Щоб вирішити ваші занепокоєння щодо IO, на сучасній ОС можливо, паралельне виконання насправді збільшить ефективність. Якщо всі процеси читають однакові блоки на диску, лише перший процес повинен потрапити на фізичне обладнання. Інші процеси часто зможуть отримати блок із дискового кешу ОС в пам'яті. Очевидно, читання з пам'яті на кілька порядків швидше, ніж читання з диска. Крім того, перевага не вимагає змін кодування.


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).

Отримавши натхнення для обробки процесів мертвих / зомбі , ось чисте рішення 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

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

Моє рішення завжди тримати задану кількість процесів запущеним, відстежувати помилки та обробляти процеси ubnterruptible / zombie:

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 in some-command do

eval `some-command for $DOMAINS` &

    job[$i]=$!

    i=$(( i + 1))

зроблено

Nдомени =echo $DOMAINS |wc -w

для i в $ (seq 1 1 $ Ndomains) do echo "wait for $ {job [$ i]}" wait "$ {job [$ i]}" done

в цій концепції буде працювати паралелізм. головне - останній рядок eval - це "&", який переводить команди у фони.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.