Сравнение create_subprocess_shell и create_subprocess_exec в asyncio
Обе функции asyncio.create_subprocess_shell и asyncio.create_subprocess_exec нужны для запуска программ прямо изнутри скрипта на Python. Эти функции очень похожи и, чтобы разобраться в отличиях, рассмотрим пример небольшого скрипта. Он запускает долго исполняющуюся программу sleep 100 и ждёт её завершения:
import asyncio
async def main():
process = await asyncio.create_subprocess_shell('sleep 100')
await process.communicate()
asyncio.run(main())
Запустим скрипт, проверим как быстро он отработает:
$ python script.py
Действительно, скрипт “думает” больше минуты — ждёт окончания работы программы sleep 100. Всё прошло как и ожидалось.
Проблемы начнутся, когда попытаемся прервать работу программы sleep 100.
В официальной документации
указано, что create_subprocess_shell запускает команду внутри шелла - командной оболочки sh.
На практике это означает, что вместо одного процесса будет запущено сразу два — один для sh и второй для sleep.
Кол-во процессов важно учитывать из-за особенностей работы sh. Дело в том, что остановка командной оболочки не приводит к остановке запущенной ей команды. Если вы попытаетесь остановить sleep через объект process, то остановите только sh, но не sleep.
Проверим это. Добавим в скрипт вызов process.terminate():
import asyncio
async def main():
process = await asyncio.create_subprocess_shell('sleep 100')
# wait till sleep will be actually launched by shell
await asyncio.sleep(1)
process.terminate()
await process.communicate() # wait till termination is over
asyncio.run(main())
Запустим в консоли скрипт, он отработает буквально за пару секунд:
$ python script.py
Сразу после проверим, остановилась ли программа sleep:
$ ps a| grep sleep
77569 pts/1 S+ 0:00 sleep 100
77721 pts/3 S+ 0:00 grep --color=auto sleep
Оказывается, sleep всё еще работает. Вызов метода process.terminate() остановил командную оболочку sh, но не запущенную внутри программу sleep.
Преимущество create_subprocess_exec
Проблем с остановкой процесса можно избежать, если вместо функции asyncio.create_subprocess_shell использовать аналогичную ей asyncio.create_subprocess_exec. Разница в том, что функция create_subprocess_shell запускает только командную оболочку sh, а asyncio.create_subprocess_exec — сразу программу sleep 100.
Тот же пример с запуском и остановкой sleep, но переписан уже с использованием create_subprocess_exec:
import asyncio
async def main():
process = await asyncio.create_subprocess_exec('sleep', '100')
# wait till sleep will be actually launched
await asyncio.sleep(1)
process.terminate()
await process.communicate() # wait till termination is over
asyncio.run(main())
В отличие от прежней версии вызов process.terminate() остановит саму команду sleep, а не только командную оболочку sh.
Особенности Mac OS
На Mac OS функция create_subprocess_shell ведёт себя иначе, чем в Linux. Командная оболочка не создаёт отдельный процесс для sleep. С одной стороны это удобно, код становится проще и приятнее. С другой стороны, если скрипт вам предстоит запускать на Linux-сервере, то тестировать его тоже следует на Linux.
Вариант первый. Запускайте скрипт внутри Docker образа.
Вариант второй. Отдельно протестируйте скрипт на Linux сервере.