Параллелизировать Bash script с максимальным количеством процессов
Предположим, что у меня есть петля в Bash:
for foo in `some-command`
do
do-something $foo
done
do-something
привязан к cpu, и у меня красивый блестящий 4-ядерный процессор. Я хотел бы иметь возможность запускать до 4 do-something
сразу.
Наивный подход выглядит следующим образом:
for foo in `some-command`
do
do-something $foo &
done
Это будет запускать все do-something
сразу, но есть пара недостатков, в основном, что-то-то, возможно, также имеет некоторые значительные операции ввода-вывода, которые все одновременно могут немного замедлить. Другая проблема заключается в том, что этот блок кода немедленно возвращается, поэтому невозможно выполнить другую работу, когда все do-something
завершены.
Как бы вы пишете этот цикл, чтобы всегда выполнялось X do-something
?
Ответы
Ответ 1
В зависимости от того, что вы хотите сделать, 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.
Ответ 2
С 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
Ответ 3
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 ...
Ответ 4
Вместо простого 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
вы просто проверяете, что содержит переменная.
Ответ 5
Здесь альтернативное решение, которое может быть вставлено в .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
, но, похоже, нет очевидного решения ждать, пока какое-либо из заданий не будет закончено, а не все их.
Ответ 6
Может быть, попробуйте использовать параллелизующую утилиту вместо перезаписи цикла? Я большой поклонник xjobs. Я все время использую xjobs для массового копирования файлов в нашей сети, как правило, при настройке нового сервера базы данных.
http://www.maier-komor.de/xjobs.html
Ответ 7
Выполнение этого права в bash
, вероятно, невозможно, вы можете сделать полуправый довольно легко. bstark
дал справедливое приближение права, но у него есть следующие недостатки:
- Разделение слов: вы не можете передавать ему какие-либо задания, в которых используются любые из следующих символов: пробелы, табуляции, символы новой строки, звезды, вопросительные знаки. Если вы это сделаете, все будет ломаться, возможно, неожиданно.
- Он полагается на остальную часть вашего script, чтобы ничего не фона. Если вы это сделаете или позже добавите что-то в script, который будет отправлен в фоновом режиме, потому что вы забыли, что вам не разрешили использовать фоновые задания из-за его фрагмента, все будет нарушено.
Другим приближением, которое не имеет этих недостатков, является следующее:
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
.
Ответ 8
Если вы знакомы с командой make
, большую часть времени вы можете выразить список команд, которые вы хотите запустить в качестве файла makefile. Например, если вам нужно запустить $SOME_COMMAND в файлах *.input, каждый из которых производит *.output, вы можете использовать makefile
INPUT = a.input b.input
OUTPUT = $(INPUT:.input=.output)
%.output : %.input
$(SOME_COMMAND) $< [email protected]
all: $(OUTPUT)
а затем просто запустите
make -j<NUMBER>
для запуска не более NUMBER команд параллельно.
Ответ 9
для 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
Ответ 10
В проекте, над которым я работаю, используется команда wait для управления процессами с параллельной оболочкой (ksh). Чтобы решить ваши проблемы с IO, на современной ОС, возможно, параллельное выполнение фактически повысит эффективность. Если все процессы считывают одни и те же блоки на диске, только первый процесс должен ударить по физическому оборудованию. Другие процессы часто смогут извлекать блок из кэша дискового пространства ОС в памяти. Очевидно, чтение из памяти на несколько порядков быстрее, чем чтение с диска. Кроме того, преимущество не требует изменений в кодировке.
Ответ 11
Это может быть достаточно хорошим для большинства целей, но не оптимальным.
#!/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
Ответ 12
Вы можете использовать простой вложенный цикл 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 равным количеству процессоров, которые у вас есть.
Ответ 13
Вот как мне удалось решить эту проблему в bash script:
#! /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
Ответ 14
Мое решение всегда поддерживать определенное количество запущенных процессов, отслеживать ошибки и обрабатывать процессы 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 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"
Ответ 15
Очень поздно на вечеринку здесь, но здесь другое решение.
Многие решения не обрабатывают пробелы/специальные символы в командах, не поддерживают постоянную работу N заданий, не используют процессор в занятых циклах или не полагаются на внешние зависимости (например, parallel
GNU).
С вдохновением для обработки мертвых/зомби, вот чистое решение 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
Ответ 16
$DOMAINS = "список некоторых доменов в командах"
для foo in some-command
сделать
eval `some-command for $DOMAINS` &
job[$i]=$!
i=$(( i + 1))
сделать
Ndomains = echo $DOMAINS |wc -w
для я в $(seq 1 1 $Ndomains)
делать echo "ждать ${job [$ i]}" wait "$ {job [$ i]}"
сделано
в этой концепции будет работать для распараллеливания. Важной вещью является последняя строка eval: '&'
которые будут помещать команды в фоны.