Дождитесь завершения фоновых заданий bash в скрипте
-
16-09-2019 - |
Вопрос
Чтобы максимально использовать процессор (я запускаю все на Debian Lenny в EC2) У меня есть простой скрипт для параллельного запуска заданий:
#!/bin/bash
for i in apache-200901*.log; do echo "Processing $i ..."; do_something_important; done &
for i in apache-200902*.log; do echo "Processing $i ..."; do_something_important; done &
for i in apache-200903*.log; do echo "Processing $i ..."; do_something_important; done &
for i in apache-200904*.log; do echo "Processing $i ..."; do_something_important; done &
...
Я вполне удовлетворен этим рабочим решением, однако я не смог понять, как написать дальнейший код, который будет выполняться только после завершения всех циклов.
Есть ли способ получить контроль над этим?
Решение
Есть bash
встроенная команда для этого.
wait [n ...]
Wait for each specified process and return its termination sta‐
tus. Each n may be a process ID or a job specification; if a
job spec is given, all processes in that job’s pipeline are
waited for. If n is not given, all currently active child pro‐
cesses are waited for, and the return status is zero. If n
specifies a non-existent process or job, the return status is
127. Otherwise, the return status is the exit status of the
last process or job waited for.
Другие советы
Использование GNU Parallel сделает ваш скрипт еще короче и, возможно, более эффективным:
parallel 'echo "Processing "{}" ..."; do_something_important {}' ::: apache-*.log
При этом будет выполняться по одному заданию на ядро процессора и продолжаться до тех пор, пока не будут обработаны все файлы.
Ваше решение в основном разделит задания на группы перед запуском.Здесь 32 задания в 4 группах:
Вместо этого GNU Parallel запускает новый процесс, когда он завершается, сохраняя процессоры активными и, таким образом, экономя время:
Чтобы узнать больше:
- Посмотрите вступительное видео для краткого ознакомления:https://www.youtube.com/playlist?list=PL284C9FF2488BC6D1
- Пройдитесь по учебнику (man parallel_tutorial).Ваша командная строка полюбит вас за это.
Мне пришлось сделать это недавно, и в итоге я получил следующее решение:
while true; do
wait -n || {
code="$?"
([[ $code = "127" ]] && exit 0 || exit "$code")
break
}
done;
Вот как это работает:
wait -n
завершается, как только завершается одно из (потенциально многих) фоновых заданий.Он всегда принимает значение true, и цикл продолжается до тех пор, пока:
- Код выхода
127
:последнее фоновое задание успешно завершено.В этом случае мы игнорируем код выхода и выходим из подшколы с кодом 0. - Ни одно из фоновых заданий не удалось выполнить.Мы просто выходим из вложенной оболочки с этим кодом выхода.
С set -e
, это гарантирует, что скрипт завершится досрочно и пройдет через код выхода любого неудачного фонового задания.
Это мое грубое решение:
function run_task {
cmd=$1
output=$2
concurency=$3
if [ -f ${output}.done ]; then
# experiment already run
echo "Command already run: $cmd. Found output $output"
return
fi
count=`jobs -p | wc -l`
echo "New active task #$count: $cmd > $output"
$cmd > $output && touch $output.done &
stop=$(($count >= $concurency))
while [ $stop -eq 1 ]; do
echo "Waiting for $count worker threads..."
sleep 1
count=`jobs -p | wc -l`
stop=$(($count > $concurency))
done
}
Идея состоит в том, чтобы использовать «задания», чтобы увидеть, сколько детей активны в фоновом режиме, и подождать, пока это число упадет (дочерний элемент выйдет).Как только дочерний элемент существует, можно запустить следующую задачу.
Как видите, здесь также имеется дополнительная логика, позволяющая избежать многократного выполнения одних и тех же экспериментов/команд.Это делает работу за меня..Однако эту логику можно либо пропустить, либо улучшить (например, проверить метки времени создания файла, входные параметры и т. д.).