Сравнение 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 сервере.


Попробуйте бесплатные уроки по Python

Получите крутое код-ревью от практикующих программистов с разбором ошибок и рекомендациями, на что обратить внимание — бесплатно.

Переходите на страницу учебных модулей «Девмана» и выбирайте тему.