Почему команда django runserver запускает 2 процесса? Для чего они нужны? И как отличить каждый из них в коде?

В процессе создания автономного приложения Django, которое будет выполнять задачи в фоновом режиме как отдельный демон Thread, я столкнулся с проблемой, потому что казалось, что при запуске сервера Django есть два MainThreads, каждый Thread имеет свой id. После более детального изучения проблемы выяснилось, что это происходит потому, что на самом деле это два процесса.

Эксперимент:

  1. Запустите django-admin startproject example && cd example, чтобы начать новый проект.
  2. Отредактируйте example/settings.py, добавив imoprt os, если он еще не импортирован, а затем добавьте строку print(f"Current processes ID:{os.getpid()}")
  3. .
  4. Запустите python manage.py runserver и посмотрите на результат
Current processes ID:426286
Current processes ID:426288
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 17 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

August 21, 2022 - 15:30:42
Django version 2.2.12, using settings 'example.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C.
  1. Измените любой файл (например, просто добавьте новую строку в settings.py), сохраните и посмотрите на вывод
  2. .
/pathtoproject/example/example/settings.py changed, reloading.
Current processes ID:426417
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).

You have 17 unapplied migration(s). Your project may not work properly until you apply the migrations for app(s): admin, auth, contenttypes, sessions.
Run 'python manage.py migrate' to apply them.

August 21, 2022 - 15:32:07
Django version 2.2.12, using settings 'example.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CONTROL-C. 

Наблюдение

manage.py runserver запускает 2 процесса. Один из них остается до остановки сервера, а другой завершается и создается заново с другим идентификатором каждый раз, когда проект перезагружается из-за изменений в файлах.

Подробности о версии

Это было протестировано на нескольких версиях Django 2, 3 и 4, и всегда запускается 2 процесса, как объяснялось выше.

Один из 2 процессов предназначен для автозагрузки.
Используйте --noreload с runserver.

Официальной документации по StateReloader не так много, но, к счастью, я смог разобраться в коде. Итак, при запуске Django, он в основном использует встроенную библиотеку python subprocess для создания нового процесса python с точно такими же аргументами, как в этой строке в исходном коде. Значение переменной args будет примерно таким

['/usr/bin/python', 'manage.py', 'runserver']

Теперь следует задать очень важный вопрос: если запуск python manage.py runserver запускает новый экземпляр python manage.py runserver, то что мешает ему пройти через замкнутый цикл, в котором он создает новый процесс снова и снова, пока не произойдет сбой ОС после окончания оперативной памяти?

Ответ на этот вопрос также является ответом на главный вопрос и находится в этой строке в исходном коде

        if os.environ.get(DJANGO_AUTORELOAD_ENV) == "true":

Django использует переменную окружения, определенную как DJANGO_AUTORELOAD_ENV, чтобы определить, является ли текущий процесс процессом Django, инициированным Statereloader, или процессом самого Statereloader.

Значение DJANGO_AUTORELOAD_ENV, определенное в этой строке, равно "RUN_MAIN" и остается неизменным со времен Django 2 до сегодняшнего дня в Django 4.

Итак, если вы добавите следующий пример кода в settings.py:

if os.environ.get('RUN_MAIN') == 'true':
    print("This process has been initiated by Statereloader")

Оператор print будет выполнен только один раз при запуске приложения, однако он будет выполняться снова при каждой перезагрузке приложения. Если вы хотите, чтобы оно выполнялось только один раз при запуске приложения и больше никогда, независимо от Statereloader, то оно должно выглядеть следующим образом:

if os.environ.get('RUN_MAIN') != 'true':
    print("This is the original process and will never restart")

Также, полезный совет, если вам нужно запустить фоновую задачу вместе с Django без вмешательства в работу Django, вы можете запустить поток демона внутри метода AppConfig.ready AFTER, проверяя переменную окружения RUN_MAIN, как показано ниже.

from django.apps import AppConfig
import threading
import os


class RockNRollConfig(AppConfig):
    # ...

    def ready(self):
        if os.environ.get('RUN_MAIN') == 'true':
             threading.Thread(
                  name="YourDaemonThread",
                  target=your_thread_function,
                  daemon=True
             ).start()

Таким образом, вы можете быть уверены в том, что:

  1. Одновременно будет запущен только один экземпляр вашего потока (если вы не запустите несколько экземпляров сервера Django)
  2. .
  3. Ваш поток будет перезапускаться вместе со всем остальным в приложении Django всякий раз, когда Statereloader воссоздает процесс.
Вернуться на верх