В большинстве командных оболочек команды выполняются по умолчанию последовательно. И это, в принципе, нормально. Потому что человек с системой взаимодействует последовательно, обычно нет необходимости несколько команд выполнять параллельно. Bash в этом смысле тоже не исключение. Но при автоматизации возможность параллельного выполнения может быть полезной. Давайте посмотрим, как организовать параллельное выполнение в bash.
Использование фонового режима
Для организации параллельной работы нескольких программ часто используется запуск в фоновом режиме при помощи знака амперсанда — &. Например:
command1 &
Команда будет работать в фоне, при этом из текущей оболочки можно выполнять команды. Таким образом уже можно распараллелить какие-то действия. Можно запустить сразу несколько команд таким образом и дождаться, пока они все отработают. Для ожидания запущенных дочерних процессов используется команда wait. Эта команда без параметров ожидает окончания работы всех дочерних процессов, соответственно, для ожидания окончания 5 процессов понадобится выполнить команду всего 1 раз. В принципе, это легко реализуется через цикл. Например, так:
for i in {1..5} do # запуск одного фонового процесса sleep 10 && echo $i & done # ожидание окончания работы wait echo Finished
И результат работы этого скрипта:
$ ./wait5.sh 1 5 4 2 3 Finished
Как видите, с виду одинаковые команды завершились не в том порядке, в котором мы их запустили. Давайте посмотрим теперь общее время выполнения скрипта.
$ time ./wait5.sh 4 5 2 3 1 Finished real 0m10.029s user 0m0.000s sys 0m0.008s
Общее время работы скрипта чуть больше 10 секунд, что доказывает, что наши команды выполнились параллельно, а увеличение времени выполнения говорит о том, что они были запущены не в одно и то же время, были небольшие таймауты между запусками. Но они были очень маленькими, поэтому такой запуск пяти процессов занимает практически такое же время, как запуск одного.
Использование пайпа
При использовании пайпов, которые перенаправляют вывод одной программы на вход другой, процессы выполняются параллельно. Отсюда вытекает еще один способ параллельного выполнения нескольких команд — перенаправить между ними какие-то данные с помощью пайпа. Например:
program1 | program2 | program3
Тут важно помнить вот что: если вам не нужно передавать реально какие-то данные между программами, то надо предварительно убедиться, что входные данные эти программы получат в виде опций командной строки и не будут использовать те данные, которые были переданы им с помощью пайпа. Хотя здесь, в принципе, возможен такой вариант:
command1 --option > dev/null | command2 param1 param2 > /dev/null | command3
Этот вариант до использования в скриптах, естественно, нужно обязательно проверить в ручном режиме и посмотреть в страницах руководств используемых программ (если там есть такая информация), что имеет больший приоритет — опции командной строки или стандартный поток ввода, потому что некоторые программы могут игнорировать командную строку, если данные передаются через стандартный поток ввода.
Параллельное выполнение и ограничение количества фоновых задач в скрипте
Давайте рассмотрим такую практическую задачу — запустить 100 процессов параллельно, но так, чтобы работало одновременно не более 10 процессов. В общем, достаточно простая задача. Предположим, что все процессы работают произвольное количество времени. Пусть запуск одной задачи будет выглядеть как запуск команды sleep со случайным параметром от 0 до 29. Тогда скрипт будет выглядеть следующим образом:
#!/bin/bash RANDOM=10 JOBS_COUNTER=0 MAX_CHILDREN=10 MY_PID=$$ for i in {1..100} do echo Cycle counter: $i JOBS_COUNTER=$((`ps ax -Ao ppid | grep $MY_PID | wc -l`)) while [ $JOBS_COUNTER -ge $MAX_CHILDREN ] do JOBS_COUNTER=$((`ps ax -Ao ppid | grep $MY_PID | wc -l`)) echo Jobs counter: $JOBS_COUNTER sleep 1 done sleep $(($RANDOM % 30)) & done echo Finishing children ... # wait for children here while [ $JOBS_COUNTER -gt 1 ] do JOBS_COUNTER=$((`ps ax -Ao ppid | grep $MY_PID | wc -l`)) echo Jobs counter: $JOBS_COUNTER sleep 1 done echo Done
Смысл этого скрипта в целом такой: ограничиваем максимально число дочерних фоновых процессов так, чтобы их одновременно было не более 10. Как только один процесс заканчивает свою работу, запускаем следующий. И так далее, пока не выполним 100 фоновых задач. Для порядка отслеживаем в скрипте окончание работы дочерних процессов после запуска последних, только потом заканчиваем работу самого скрипта.
Таким простым способом можно ограничить количество одновременно запущенных фоновых задач в скрипте и при этом отслеживать, сколько из них в данный момент работают. Чтобы было более понятно, запустите скрипт и увидите, когда запускается следующие итерации цикла, и когда изменяется количество дочерних процессов.
Надо заменить JOBS_COUNTER=$((`ps ax -Ao ppid | grep $MY_PID | wc -l`)) на
JOBS_COUNTER=$(( `ps ax -Ao ppid | grep $MY_PID | wc -l` — 3 )).
Т.к. вызов `ps ax -Ao ppid | grep $MY_PID | wc -l` порождает еще дополнительных 3 процесса.
Иначе скрипт никогда не завершить свое выполнение.
На самом деле этот вызов порождает только один дочерний процесс оболочки, в котором выполняются эти три команды, не являющиеся прямыми потомками скрипта. Можете запустить скрипт из статьи и удостовериться, что он все-таки завершает свою работу. При этом действительно, этот подпроцесс считается, но это 1, а никак не 3. В статье написано «Смысл этого скрипта в целом такой: ограничиваем максимально число дочерних фоновых процессов так, чтобы их одновременно было не более 10», поэтому соглашусь только с тем, что фоновых процессов все-таки выполняется не более 9, а не 10. 10-й — это не фоновый child process.
Здравствуйте! А у меня обратная задача. Нужно, чтобы все команды были выполнены строго последовательно и следующая команда выполнялась только по завершению предыдущей. Вот скрипт, он рабочий, но сайт перестает работать после того, как выполняется данный скрипт:
#!/bin/bash
chown mysql:mysql ./data/*
find ./data -type f -exec chmod 660 {} \;
service mysql stop
mv ./data/* /var/lib/mysql/data
service mysql start
exit
Подозреваю, что mysql не успевает остановиться при команде service mysql stop, когда уже стала выполняться команда mv
Подскажите, как это можно исправить? Спасибо большое!
Добрый день.
Все команды в этом скрипте выполняются строго последовательно, и следующая команда выполняется только тогда, когда предыдущая закончила свою работу. Теоретически да, процесс mysqld может не успеть завершить свою работу, но чтобы в этом убедиться, надо более внимательно последить за процессами. Попробуйте сделать следующее: добавить после команды «service mysql stop» следующую строчку:
while pgrep mysqld; do sleep 1; done
Это позволит убедиться, что процессов mysqld в системе нет. И, если это так, значит проблема в другом месте. Можно посмотреть, что пишется в лог mysql при остановке и старте сервиса
Максим, спасибо огромное! Вы меня выручили!
Вот окончательный вариант скрипта (заодно упростил строчку с командой chmod), отрабатывает как задумано:
#!/bin/bash
chown mysql:mysql ./data/*
chmod 660 ./data/*
service mysql stop
while pgrep mysqld; do sleep 1; done
mv ./data/* /var/lib/mysql/data
service mysql start
exit
Может еще кому-нибудь такой скрипт пригодится, когда надо с локального компа залить по FTP обновления базы в промежуточный каталог на сервере, а затем безопасно и почти мгновенно обновить файлы БД.
Остановка mysqld действительно не мгновенная, даже визуально заметно, что когда в консоли вводишь команду service mysql stop, то проходит примерно секунда по времени, прежде чем появляется приглашение от Ubuntu на ввод новой команды.
Всегда пожалуйста.
Если чем-то еще могу помочь — пишите
Спасибо, за статьи.
Если делать программу для работы (без вывода реального числа процессов) можно было бы обойтись без grep, например так:
MAX_CHILDREN=5
ZERO_LEVEL=$(ps —ppid $MY_PID |wc -l)
MAX_CHILDREN=$(($MAX_CHILDREN + $ZERO_LEVEL))
#skip
JOBS_COUNTER=$(ps —ppid $MY_PID | wc -l)
while [ $JOBS_COUNTER -ge $MAX_CHILDREN
Добрый день, Максим.
Есть ли вероятность того, что скрипт
`ps ax -Ao ppid | grep $MY_PID | wc -l`
будет возвращать кроме дочерних процессов еще и процессы, PPID которых будет включать в себя значение $MY_PID? Можно ли избежать этого, используя следующую конструкцию
ps ax -Ao ppid | tr -d [[:blank:]] | grep -xc $MY_PID
то есть проверяя полное совпадение (и сразу подсчитывая количество таких совпадений) без учета WHITESPACES.
Добрый день.
Да, есть такая вероятность. Отличная мысль :)