Многопоточное программирование в Bash: основы и управление фоновыми процессами
TL;DR
В Bash многопоточность реализуется через фоновые процессы: оператор & запускает команду в фоне, ${!} возвращает PID последнего фонового процесса, а wait ждёт завершения процессов. Эти примитивы позволяют создавать параллельные рабочие сценарии, но требуют контроля (trap, kill, wait -n, семафоры) для надёжности и предсказуемости.
Быстрые ссылки
- Что такое многопоточное программирование?
- Управление многопоточными и фоновыми процессами
- Заключение и практические приёмы

Что такое многопоточное программирование?
Иллюстрация часто говорит больше, чем тысяча слов — особенно если нужно показать разницу между последовательным и параллельным выполнением команд в Bash.
Последовательное (однопоточное) выполнение:
sleep 1Параллельное (многопоточное) выполнение в простейшем виде:
sleep 1 & sleep 1В первом случае выполняется одна команда sleep, оболочка ждёт завершения и возвращает управление. Во втором случае первый sleep запускается в фоне благодаря &, а оболочка сразу же переходит к выполнению следующей команды. Таким образом обе команды sleep выполняются одновременно (в рамках разных процессов/подоболочек).
Оператор ; завершает команду, затем запускает следующую; && запускает следующую команду только при успешном завершении предыдущей; || — только при ошибке предыдущей. Только & запускает процесс в фоне и продолжает выполнение текущей оболочки без ожидания.
Когда вы запускаете команду с &, оболочка выведет идентификатор задания и PID, например:
[1] 445317
где 445317 — PID фонового процесса, а [1] — номер задания в текущей оболочке.
Чтобы убедиться, что два sleep выполняются параллельно, можно измерить время:
time sleep 1; echo 'done'и
time $(sleep 1 & sleep 1); echo 'done'
В первом примере общее время ≈1 секунда. Во втором — тоже ≈1 секунда, потому что два sleep 1 шли одновременно. Небольшая разница во времени объясняется накладными расходами на создание под-оболочки и запуск фонового процесса.
Важно понимать: в контексте Bash «поток» чаще всего обозначает отдельный процесс или подпроцесс (subshell). Bash не предоставляет легковесных потоков уровня pthreads; параллельность достигается через запуск нескольких процессов.
Управление многопоточными и фоновыми процессами
В реальных скриптах фоновые процессы обычно делают полезную работу — выполняют длительные задачи параллельно с основной логикой. Но с параллельностью приходит ответственность: нужно отслеживать PIDs, корректно ждать завершения, обрабатывать ошибки и обеспечивать чистое завершение.
Простой пример — запуск пяти фонов и вывод PID-ов:
#!/bin/bash
sleep 10 &
sleep 600 &
sleep 1200 &
sleep 1800 &
sleep 3600 &Если вы выполните этот скрипт извне (./rest.sh), команда jobs в вашей интерактивной оболочке может не показать этих фоновых заданий, потому что они принадлежат подпроцессу (subshell), созданному для запуска скрипта.
Чтобы увидеть задания внутри скрипта, добавьте jobs в конец скрипта — этот jobs выполнится в той же подпроцессной оболочке, где запущены background-процессы:

Получение PID последнего фонового процесса: ${!}
В Bash переменная ${!} содержит PID последнего запущенного в фоне процесса. Пример:
#!/bin/bash
sleep 10 &
echo ${!}
sleep 600 &
echo ${!}
sleep 1200 &
echo ${!}
sleep 1800 &
echo ${!}
sleep 3600 &
echo ${!}
Это выводит пять PID-ов практически сразу после запуска скрипта — полезно для дальнейшего управления процессами.
Команда wait
Команда wait позволяет основной оболочке приостановить выполнение до завершения указанных процессов. Она принимает PID или номер задания. Простой шаблон использования:
#!/bin/bash
sleep 10 &
T1=${!}
sleep 600 &
T2=${!}
sleep 1200 &
T3=${!}
sleep 1800 &
T4=${!}
sleep 3600 &
T5=${!}
echo "This script started 5 background threads which are currently executing with PID's ${T1}, ${T2}, ${T3}, ${T4}, ${T5}."
wait ${T1}
echo "Thread 1 (sleep 10) with PID ${T1} has finished!"
wait ${T2}
echo "Thread 2 (sleep 600) with PID ${T2} has finished!"
Этот код поочерёдно ожидает завершения отдельных фоновых процессов. wait можно вызывать без аргументов — тогда оболочка ждёт всех дочерних процессов. Также доступен wait -n (в современных версиях Bash), который возвращает при завершении первого из фоновых процессов.
Распространённые паттерны управления конкуренцией
- Wait на всех процессов:
# собрать PID-ы в массив и потом ждать их всех
pids=()
cmd1 & pids+=(${!})
cmd2 & pids+=(${!})
for pid in "${pids[@]}"; do
wait ${pid}
done- Использование wait -n для обработки первого завершившегося процесса (полезно для динамической генерации задач и перераспределения):
# пример: ждать первого завершившегося и запускать новую задачу
max_parallel=4
pids=()
for job in ${jobs_list}; do
while [ ${#pids[@]} -ge $max_parallel ]; do
# дождаться любого завершившегося процесса
wait -n
# очистить массив pids от завершившихся (можно фильтровать с помощью kill -0)
new_pids=()
for pid in "${pids[@]}"; do
if kill -0 ${pid} 2>/dev/null; then
new_pids+=(${pid})
fi
done
pids=(${new_pids[@]})
done
do_work "${job}" &
pids+=(${!})
done
# дождаться оставшихся
wait- Ограничение параллелизма через семафор (mkfifo + &):
# примитивный семафор на N слотов
N=4
tmpf=/tmp/sem.$$
mkfifo ${tmpf}
exec 3<>${tmpf}
rm ${tmpf}
for ((i=0;i<$N;i++)); do echo >&3; done
for job in ${jobs_list}; do
read -u 3
{
do_work "${job}"
echo >&3
} &
done
wait
exec 3>&-Обработка сигналов и чистое завершение
Когда ваш скрипт запускает фоновые процессы, важно корректно обрабатывать сигналы (SIGINT, SIGTERM) и при необходимости завершать дочерние процессы. Пример:
#!/bin/bash
children=()
trap 'echo "Signal received, killing children..."; for p in "${children[@]}"; do kill "$p" 2>/dev/null; done; exit 1' SIGINT SIGTERM
long_running &
children+=(${!})
other_task &
children+=(${!})
waitБез такого trap фоновые дочерние процессы могут остаться «висячими» после завершения главного скрипта (особенно на службах, cron и т. п.). Для демонстрационного использования можно также применять disown или nohup, но это не решает проблему мониторинга и корректного завершения.
Различие между job control и системными службами
Job control (jobs, fg, bg) — это функциональность интерактивной оболочки. Скрипты, запущенные вне интерактивной сессии (например, systemd unit или cron), обычно не имеют job-терминала и управляющих возможностей job control. Для постоянных фоновых задач лучше использовать systemd, cron, at или docker/kubernetes.
Практические рекомендации и распространённые ошибки
- Не полагаться на
jobsвне интерактивной оболочки — используйте${!}и PID-ы. - Внимательно работайте с под-оболочками (подоболочка = subshell). Команды в
()выполняются в отдельной подпроцессной оболочке; они не влияют на переменные родительской оболочки. - Проверяйте успешность фоновых задач:
wait $pidвозвращает код завершения процесса. - Используйте
kill -0 $pidдля проверки, жив ли процесс. - Для параллельной обработки большого числа задач предпочтительнее GNU parallel или xargs -P, они уже реализуют становление очереди, ретрай и логирование.
Примеры альтернативных инструментов:
- GNU parallel — удобен для параллельных вызовов команд с ограничением числа одновременных задач.
- xargs -P N — позволяет выполнять N параллельных процессов для списка аргументов.
- background + systemd timer/units — для долговременных, управляемых процессов.
Когда подход не работает (ограничения и подводные камни)
- Если ваш рабочий процесс критичен к памяти и CPU, простое параллельное запускание команд может привести к перегрузке узла. Нужно ограничивать параллелизм и контролировать ресурсы.
- Если задачи зависят от общих файлов или ресурсов, параллельный доступ может вызвать гонки (race conditions). Используйте блокировки (flock) или базы данных/координацию.
- Встроенная обработка ошибок в Bash менее удобна, чем в языках с полноценной параллелизацией — подумайте о переходе на Python/Go для сложной логики параллельности.
Полезные сниппеты и шпаргалка
- Запуск команды в фоне и получение PID:
cmd &
pid=${!}- Ожидание всех фоновых процессов:
wait- Ожидание конкретного процесса и получение его кода выхода:
wait ${pid}
rc=$?- Проверка, жив ли процесс:
if kill -0 ${pid} 2>/dev/null; then
echo "alive"
else
echo "not running"
fi- Динамическое ограничение параллельности (wait -n):
# Bash 4.3+
max=8
for task in ${tasks}; do
while [ $(jobs -rp | wc -l) -ge $max ]; do
wait -n
done
run_task "$task" &
done
waitМентальные модели и эвристики
- Подсистема: думайте о каждой фоновой задаче как о независимом процессе с собственным PID.
- Управление ресурсами: число одновременных задач — основной рычаг контроля нагрузки.
- Надёжность: любая параллелизация должна предусматривать обработку ошибок и корректную очистку (cleanup) при сигнале.
Роль — чек-листы перед запуском параллельного скрипта
Разработчик:
- Проведите локальное тестирование с небольшим числом параллельных задач.
- Добавьте логирование и обработку ошибок (exit code через wait).
- Документируйте ожидания по ресурсам.
Оператор/DevOps:
- Проверяйте использование CPU и памяти при нагрузке.
- Настройте мониторинг для процессов и нотификации о сбоях.
- Используйте systemd/unit-файлы для долгоживущих задач.
QA/Тестировщик:
- Тестируйте сценарии отказа (перезапуск процесса, прерывание скрипта).
- Проверяйте гонки при одновременном доступе к файлам/БД.
Короткий глоссарий
- PID — идентификатор процесса в системе.
- Subshell (под-оболочка) — отдельный процесс, созданный оболочкой для выполнения набора команд.
- job control — функционал интерактивной оболочки для управления фоновыми заданиями (
jobs,fg,bg). - wait -n — опция команды wait, возвращающая управление при завершении первого дочернего процесса.
Decision flow: выбрать стратегию параллелизации
flowchart TD
A[Нужна параллельность?] -->|Нет| B[Последовательный сценарий]
A -->|Да| C[Небольшой набор задач '<50'?]
C -->|Да| D[Используем & + wait / семафор в Bash]
C -->|Нет| E[Много задач или сложная логика?]
E -->|Да| F[Используем GNU parallel / xargs -P / очередь задач]
E -->|Нет| D
D --> G[Добавить обработку сигналов и контроль PIDs]
F --> H[Настроить логи, ретрай и мониторинг]Заключение
В Bash многопоточность реализуется через создание фоновых процессов. Оператор &, переменная ${!} и команда wait — базовый набор средств для организации параллельной работы. Для надёжных скриптов добавляйте обработку сигналов, логику ограничения параллельности и проверки состояния процессов. Для крупных задач рассматривайте специализированные инструменты (GNU parallel, xargs -P) или перенос логики в язык с продвинутыми средствами параллелизма.
Если статья была полезна, рекомендуем ознакомиться также с материалами по завершению процессов в Bash и управлению задачами в systemd.
Основные выводы
- Оператор
&запускает процесс в фоне;${!}даёт PID последнего фонового процесса;waitожидает завершения. - Проверяйте и контролируйте параллелизм, чтобы избежать перегрузки ресурсов.
- Обрабатывайте сигналы и корректно завершайте дочерние процессы.
- Для масштабируемых рабочих нагрузок используйте GNU parallel или системные менеджеры задач.
Похожие материалы
makecab.exe — исправление высокой загрузки CPU
Кнопка «Мне повезёт» в Google — как пользоваться
Как разрешить всплывающие окна на iPhone
Начать секцию с нечётной страницы в Word 2013
Пересылка писем в задачи ClickUp