Поиск по сайту:

Как использовать многопоточную обработку в сценариях Bash


Многопоточное программирование всегда интересовало разработчиков для повышения производительности приложений и оптимизации использования ресурсов. Это руководство познакомит вас с основами многопоточного кодирования Bash.

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

Картинка стоит тысячи слов, и это верно, когда дело доходит до демонстрации разницы между программированием с одним (1) потоком и многопоточным (> 1) программированием в Bash:

sleep 1
sleep 1 & sleep 1

Наша первая установка многопоточного программирования или однострочный мини-скрипт не могли быть проще; в первой строке мы засыпаем на одну секунду с помощью команды sleep 1. Что касается пользователя, один поток выполнял одиночный сон продолжительностью в одну секунду.

Во второй строке у нас есть две односекундные команды сна. Мы соединяем их с помощью разделителя &, который действует не только как разделитель между двумя командами sleep, но и как индикатор Bash для запуска первой команды в фоновая ветка.

Обычно команду завершают точкой с запятой (;). Это приведет к выполнению команды и только затем переходу к следующей команде, указанной после точки с запятой. Например, выполнение sleep 1; sleep 1 занял бы чуть более двух секунд — ровно одна секунда для первой команды, одна секунда для второй и небольшая системная нагрузка для каждой из двух команд.

Однако вместо завершения команды точкой с запятой можно использовать другие терминаторы команд, которые Bash распознает, такие как &, && и ||. Синтаксис && совершенно не связан с многопоточным программированием, он просто делает это; продолжить выполнение второй команды только в том случае, если первая команда была успешной. || является противоположностью && и будет выполнять вторую команду, только если первая команда не удалась.

Возвращаясь к многопоточному программированию, использование & в качестве ограничителя команды инициирует фоновый процесс, выполняющий предшествующую ему команду. Затем он немедленно приступает к выполнению следующей команды в текущей оболочке, оставляя фоновый процесс (поток) выполняться сам по себе.

В выводе команды мы видим запущенный фоновый процесс (на что указывает [1] 445317, где 445317 — идентификатор процесса или PID только что запущенного фонового процесса и [1] указывает, что это наш первый фоновый процесс), и впоследствии он завершается (как указано [1]+ Done sleep 1).

Если вы хотите просмотреть дополнительный пример обработки фоновых процессов, ознакомьтесь с нашей статьей Bash Automation and Scripting Basics (Part 3). Кроме того, могут быть интересны Bash Process Termination Hacks.

Давайте теперь докажем, что мы эффективно запускаем два процесса sleep одновременно:

time sleep 1; echo 'done'
time $(sleep 1 & sleep 1); echo 'done'

Здесь мы запускаем наш процесс sleep под time и видим, как наша однопоточная команда выполнялась ровно 1,003 секунды до того, как было возвращено приглашение командной строки.

Однако во втором примере это заняло примерно столько же времени (1,005 секунды), несмотря на то, что мы выполняли два периода (и процесса) сна, хотя и не последовательно. Мы снова использовали фоновый процесс для первой команды sleep, что привело к (полу)параллельному выполнению, т. е. многопоточному.

Мы также использовали оболочку подоболочки ($ (...)) вокруг наших двух команд сна, чтобы объединить их вместе в time. Как мы видим, наш вывод done отображается через 1,005 секунды, и, следовательно, две команды sleep 1 должны выполняться одновременно. Интересным является очень небольшое увеличение общего времени обработки (0,002 секунды), которое можно легко объяснить временем, необходимым для запуска подоболочки, и временем, необходимым для запуска фонового процесса.

Многопоточное (и фоновое) управление процессами

В Bash многопоточное кодирование обычно включает фоновые потоки из основного однострочного сценария или полного сценария Bash. По сути, многопоточное кодирование в Bash можно представить как запуск нескольких фоновых потоков. Когда вы начинаете кодировать с использованием нескольких потоков, быстро становится ясно, что такие потоки обычно требуют некоторой обработки. Например, возьмем вымышленный пример, в котором мы запускаем пять одновременных периодов (и процессов) сна в сценарии Bash;

#!/bin/bash

sleep 10 & 
sleep 600 & 
sleep 1200 & 
sleep 1800 & 
sleep 3600 &

Когда мы запускаем скрипт (сделав его исполняемым с помощью chmod +x rest.sh), мы не видим никакого результата! Даже если мы выполним jobs (команда, которая показывает все выполняемые фоновые задания), вывода не будет. Почему?

Причина в том, что оболочка, которая использовалась для запуска этого скрипта (т. е. текущая оболочка), не является той же оболочкой (или тем же потоком; если начать думать о подоболочках как о потоках самих по себе), которая выполняла фактический сон команды или поместить их в фоновый режим. Скорее это была (под)оболочка, которая запускалась при выполнении ./rest.sh.

Давайте изменим наш скрипт, добавив jobs внутрь скрипта. Это гарантирует, что jobs будет выполняться внутри (под)шелла, где это необходимо, того же самого, где были запущены периоды (и процессы) сна.

На этот раз мы можем увидеть список запущенных фоновых процессов благодаря команде jobs в конце скрипта. Мы также можем видеть их PID (идентификаторы процессов). Эти PID очень важны, когда речь идет об обработке и управлении фоновыми процессами.

Другой способ получить идентификатор фонового процесса — запросить его сразу после перевода программы/процесса в фоновый режим:

#!/bin/bash

sleep 10 & 
echo ${!}
sleep 600 & 
echo ${!}
sleep 1200 & 
echo ${!}
sleep 1800 & 
echo ${!}
sleep 3600 &
echo ${!}

Подобно нашей команде jobs (теперь с новыми PID, поскольку мы перезапустили наш скрипт rest.sh), благодаря Bash $ {!} переменная, мы теперь увидим пять PID, отображаемых почти сразу после запуска скрипта: различные спящие процессы были помещены в фоновые потоки один за другим.

Команда ждать

После того, как мы запустили наши фоновые процессы, нам больше нечего делать, кроме как ждать их завершения. Однако, когда каждый фоновый процесс выполняет сложную подзадачу, и нам нужно, чтобы основной сценарий (запустивший фоновые процессы) возобновлял выполнение после завершения одного или нескольких фоновых процессов, нам нужен дополнительный код для обработки этого.

Теперь давайте расширим наш скрипт командой wait для обработки фоновых потоков:

#!/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, которые ожидают завершения PID, прикрепленного к первому и второму потокам. Через 10 секунд наш первый поток существует, и мы получаем уведомление о том же. Шаг за шагом этот скрипт будет делать следующее: запускать пять потоков почти одновременно (хотя запуск самих потоков по-прежнему последовательный, а не параллельный), где каждый из пяти sleep будет выполнять параллельно.

Затем основной сценарий (последовательно) сообщает о созданном потоке и впоследствии ожидает завершения идентификатора процесса первого потока. Когда это произойдет, он последовательно сообщит о завершении первого потока и начнет ожидание завершения второго потока и т. д.

Использование идиом Bash &, $ {!} и команды wait дает нам большую гибкость, когда речь идет о параллельном запуске нескольких потоков. (как фоновые потоки) в Bash.

Подведение итогов

В этой статье мы рассмотрели основы написания многопоточных сценариев Bash. Мы представили оператор фонового процесса (&), используя несколько простых примеров, демонстрирующих как однопоточные, так и многопоточные команды sleep. Затем мы рассмотрели, как обрабатывать фоновые процессы с помощью часто используемых идиом Bash $ {!} и wait. Мы также изучили команду jobs, чтобы увидеть запущенные фоновые потоки/процессы.

Если вам понравилось читать эту статью, взгляните на нашу статью о хаках завершения процесса Bash.