Распараллелить скрипт Bash с максимальным количеством процессов
допустим у меня есть цикл в bash:
for foo in `some-command`
do
do-something $foo
done
do-something привязан к процессору, и у меня есть хороший блестящий 4-ядерный процессор. Я хотел бы иметь возможность работать до 4 do-something ' s Сразу.
наивный подход кажется:
for foo in `some-command`
do
do-something $foo &
done
это будет работать всеdo-somethings сразу, но есть несколько недостатков, в основном, которые делают-что-то может также иметь некоторые значительные I/O, которые выполняют все сразу может немного замедлиться. Другой проблема в том, что этот блок кода возвращается немедленно, поэтому нет возможности выполнять другую работу, когда все do-somethings закончены.
как бы вы написали этот цикл, чтобы всегда было X do-somethingS работает сразу?
15 ответов:
в зависимости от того, что вы хотите сделать, 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.
С GNU Parallel http://www.gnu.org/software/parallel/ Вы можете написать:
some-command | parallel do-somethingGNU Parallel также поддерживает выполнение заданий на удаленных компьютерах. Это будет работать по одному на ядро процессора, на удаленных компьютерах, даже если они имеют разное количество ядер:
some-command | parallel -S server1,server2 do-somethingболее продвинутый пример: Здесь мы перечисляем файлы, на которых мы хотим запустить my_script. Файлы имеют расширение (возможно .jpeg). Мы хотим, чтобы вывод my_script был помещен рядом с файлами базовое имя.out (например, foo.jpeg - > foo.из.) Мы хотим запустить my_script один раз для каждого ядра компьютера, и мы хотим запустить его на локальном компьютере, тоже. Для удаленных компьютеров мы хотим, чтобы файл был обработан, переданный на данный компьютер. Когда my_script заканчивается, мы хотим foo.оттуда перевели обратно и мы потом хотим фу.jpeg и foo.выход удален с удаленного компьютера:
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
maxjobs=4
parallelize () {
while [ $# -gt 0 ] ; do
jobcnt=(`jobs -p`)
if [ ${#jobcnt[@]} -lt $maxjobs ] ; then
do-something &
shift
else
sleep 1
fi
done
wait
}
parallelize arg1 arg2 "5 args to third job" arg4 ...
вместо простого 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вы просто проверяете, что содержит переменная.
может быть, попробуйте распараллелить утилиту вместо перезаписи цикла? Я большой поклонник xjobs. Я использую xjobs все время для массового копирования файлов в нашей сети, как правило, при настройке нового сервера базы данных. http://www.maier-komor.de/xjobs.html
здесь альтернативное решение, которое может быть вставлено .bashrc и используется для повседневной один лайнер:
function pwait() { while [ $(jobs -p | wc -l) -ge ]; do sleep 1 done }использовать его, все, что нужно сделать, это поставить
&после задания и вызова pwait параметр дает количество параллельных процессов:for i in *; do do_something $i & pwait 10 doneбыло бы лучше использовать
waitвместо занятого ожидания на выходеjobs -p, но, похоже, нет очевидного решения ждать, пока любое из заданных заданий будет завершено вместо всего их.
при этом в
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команда, большую часть времени вы можете выразить список команд, которые вы хотите запустить в качестве файла makefile. Например, если вам нужно запустить $SOME_COMMAND для файлов *.вход каждого из которых производит *.вывод, вы можете использовать makefileINPUT = a.input b.input OUTPUT = $(INPUT:.input=.output) %.output : %.input $(SOME_COMMAND) $< $@ all: $(OUTPUT)а потом просто беги
make -j<NUMBER>для параллельного выполнения не более нескольких команд.
функция для 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
проект, над которым я работаю, использует ждать команда для управления параллельными процессами оболочки (KSH на самом деле). Чтобы решить ваши проблемы с IO, на современной ОС возможно параллельное выполнение фактически повысит эффективность. Если все процессы читают одни и те же блоки на диске, только первый процесс должен будет ударить по физическому оборудованию. Другие процессы часто смогут получить блок из дискового кэша ОС в памяти. Очевидно, что чтение из памяти-это несколько на порядки быстрее, чем чтение с диска. Кроме того, преимущество не требует никаких изменений в кодировке.
Это может быть достаточно хорошо для большинства целей, но не является оптимальным.
#!/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
вы можете использовать простой вложенный цикл 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 равным количеству процессоров, которые у вас есть.
вот как мне удалось решить эту проблему в скрипте bash:
#! /bin/bash MAX_JOBS=32 FILE_LIST=($(cat )) 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
мое решение всегда поддерживать заданное количество запущенных процессов, отслеживать ошибки и обрабатывать ubnterruptible / zombie процессы:
function log { echo "" } # 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="" # Number of simultaneous commands to run local commandsArg="" # 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"
$DOMAINS = " список некоторых доменов в командах" для фу в
some-commandделайeval `some-command for $DOMAINS` & job[$i]=$! i=$(( i + 1))сделал
Ndomains=
echo $DOMAINS |wc -wдля i в $(seq 1 1 $Ndomains) делать Эхо " подождите ${job[$i]}" подождите " ${job[$i]}" сделано
в этой концепции будет работать для распараллеливания. важно то, что последняя строка eval - это'&' который будет помещать команды в фоны.
Comments