Гид по технологиям

Многопоточное программирование в Bash: основы и управление фоновыми процессами

6 min read DevOps Обновлено 12 Dec 2025
Многопоточное Bash: фоновые процессы и wait
Многопоточное Bash: фоновые процессы и wait

TL;DR

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

Быстрые ссылки

  • Что такое многопоточное программирование?
  • Управление многопоточными и фоновыми процессами
  • Заключение и практические приёмы

Обложка: иллюстрация параллельного выполнения команд sleep в Bash

Что такое многопоточное программирование?

Иллюстрация часто говорит больше, чем тысяча слов — особенно если нужно показать разницу между последовательным и параллельным выполнением команд в 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'

Запуск двух sleep в параллели с одним фоновым процессом, измерение через time

В первом примере общее время ≈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-процессы:

Добавление команды jobs внутрь скрипта для отображения фоновых задач

Получение PID последнего фонового процесса: ${!}

В Bash переменная ${!} содержит PID последнего запущенного в фоне процесса. Пример:

#!/bin/bash

sleep 10 &
echo ${!}

sleep 600 &
echo ${!}

sleep 1200 &
echo ${!}

sleep 1800 &
echo ${!}

sleep 3600 &
echo ${!}

Показ PID последнего фонового процесса с помощью ${!}

Это выводит пять 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 можно вызывать без аргументов — тогда оболочка ждёт всех дочерних процессов. Также доступен 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 или системные менеджеры задач.
Поделиться: X/Twitter Facebook LinkedIn Telegram
Автор
Редакция

Похожие материалы

makecab.exe — исправление высокой загрузки CPU
Windows

makecab.exe — исправление высокой загрузки CPU

Кнопка «Мне повезёт» в Google — как пользоваться
Поиск

Кнопка «Мне повезёт» в Google — как пользоваться

Как разрешить всплывающие окна на iPhone
Mobile

Как разрешить всплывающие окна на iPhone

Начать секцию с нечётной страницы в Word 2013
Office

Начать секцию с нечётной страницы в Word 2013

Пересылка писем в задачи ClickUp
Продуктивность

Пересылка писем в задачи ClickUp

Обновление Windows 7 сразу — Convenience Rollup
Windows

Обновление Windows 7 сразу — Convenience Rollup