Python изнутри. Структуры процесса

http://tech.blog.aknin.name/2010/05/26/pythons-innards-pystate/
  • Перевод
  • Tutorial
1. Введение
2. Объекты. Голова
3. Объекты. Хвост
4. Структуры процесса

Продолжаем перевод цикла статей о внутренностях Питона. Если вы хоть раз задавались вопросом «а как же оно устроено?», обязательно читайте. Автор проливает свет на многие интересные и важные аспекты устройства языка.

В предыдущих частях мы говорили об объектной системе Питона. Тема ещё не исчерпана, но давайте пойдём дальше.

Когда я размышляю о реализации Питона, я представляю себе огромный конвейер, по которому движутся коды машинных операций, которые затем попадают в гигантский завод, где повсюду возвышаются градирни и башенные краны, — и меня просто переполняет желание подойти поближе. В этой части мы поговорим о структурах состояния интерпретатора и состояния потока (./Python/pystate.c). Сейчас нам нужно заложить фундамент, чтобы потом было легче понять, как исполняется байткод. Совсем скоро мы узнаем, как устроены фреймы, пространства имён и объекты кода. Но для начала давайте поговорим о тех структурах данных, которые связывают всё воедино. Учтите, я предполагаю наличие хотя бы поверхностного понимания устройства операционных систем и знания хотя бы таких терминов, как ядро, процесс, поток и т. п.

Во многих операционных системах пользовательский код исполняется в потоках, которые живут в процессах (это верно для большинства *nix-систем и для «современных» версий Windows). Ядро ответственно за подготовку и удаление процессов и потоков, а также за определение того, какой поток на каком логическом CPU будет исполняться. Когда процесс вызывает функцию Py_Initialize, на сцену выходит другая абстракция, интерпретатор. Любой Python-код, запускаемый в процессе, привязан к интерпретатору. Об интерпретаторе можно думать как об основе всех прочих концепций, которые мы будем обсуждать. Питон поддерживает инициализацию двух (и более) интерпретаторов в одном процессе. Несмотря на то, что эта возможность редко используется на практике, я буду её учитывать. Как было сказано, код исполняется в потоке (или потоках). Не исключение и виртуальная машина Питона (VM). При этом сама VM имеет поддержку потоков, т.е. у Питона есть своя абстракция для представления потоков. Реализация этой абстракции полностью полагается на механизмы ядра. Таким образом, и ядро, и Питон имеют представление о каждом из Python-потоков. Эти потоки управляются ядром и исполняются как отдельные потоки параллельно всем прочим потокам в системе. Ну… почти параллельно.

До сих пор мы не обращали внимания на слона в нашей посудной лавке. Зовут слона GIL (Global Interpreter Lock). По некоторым причинам многие аспекты CPython непотокобезопасны. У этого есть и преимущества (например, упрощение реализации и гарантированная атомарность многих операторов Питона), и недостатки. Основной недостаток — необходимость в механизме, предотвращающем параллельное выполнение потоков Питона, так как без такого механизма возможно повреждение данных. GIL — это блокировка уровня процесса, которую поток обязан захватить, если ему необходимо выполнять Python-код. Это ограничивает количество одновременно выполняющихся Python-потоков на одном логическом CPU до одного. Python-потоки реализуют кооперативную многозадачность, добровольно освобождая GIL и предоставляя другим потокам возможность поработать. Этот функционал встроен в цикл исполнения, т.е. не нужно специально задумываться об этой блокировке при написании обычных скриптов и некоторых расширений (им кажется, что они работают непрерывно). Учтите, что в то время, пока поток не использует API Питона (со многими такое бывает), он может работать параллельно другим Python-потокам. Чуть позже мы ещё обсудим GIL, а те, кому не терпится, могут почитать презентацию Дэвида Бизли.

Мы помним о концепциях процесса (абстракция ОС), интерпретатора (абстракция Питона) и потока (абстракция как ОС, так и Питона). Сейчас мы проделаем следующий путь: начнём с одной операции и закончим целым процессом.

Давайте ещё раз посмотрим на байткод, генерируемый выражением spam = eggs - 1 (что такое diss):

>>> diss("spam = eggs - 1")
  1           0 LOAD_NAME                0 (eggs)
              3 LOAD_CONST               0 (1)
              6 BINARY_SUBTRACT
              7 STORE_NAME               1 (spam)
             10 LOAD_CONST               1 (None)
             13 RETURN_VALUE
>>>

Помимо операции BINARY_SUBTRACT, которая и выполняет всю работу, мы видим операции LOAD_NAME (eggs) и STORE_NAME (spam). Очевидно, что для выполнения этих операций нужно место: eggs нужно откуда-то вытащить, а spam нужно куда-то убрать. На это место ссылаются внутренние структуры данных, в которых исполняется код — фрейм-объекты и объекты кода. Когда вы запускаете Python-код, на самом деле исполняются фреймы (вспомните ceval.c: PyEval_EvalFrameEx). Сейчас мы смешиваем понятия фрейм-объектов и объектов кода, так пока проще. Разницу между этими структурами поймём чуть позже. Сейчас нас больше всего интересует поле f_back фрейм-объекта. Во фрейме n это поле указывает на фрейм n-1, т.е. на фрейм, который вызвал текущий фрейм (первый фрейм в потоке указывает на NULL).

Стек фреймов уникален для каждого потока и связан со специфичной потоку структурой ./Include.h/pystate.h: PyThreadState, которая содержит указатель на текущий исполняемый фрейм потока (самый последний вызванный фрейм, вершина стека). Структура PyThreadState выделяется и инициализируется для каждого Python-потока в процессе функцией _PyThreadState_Prealloc прямо перед тем, как созданный поток запрашивается из ОС (./Modules/_threadmodule.c: thread_PyThread_start_new_thread и >>> from _thread import start_new_thread). В процессе могут создаваться и такие потоки, которые не управляются интерпретатором; у них нет структуры PyThreadState, и они не должны обращаться к Python API. Такое бывает в основном во встраиваемых приложениях. Но такие потоки можно «питонизировать», чтобы появилась возможность исполнять в них Python-код, при этом нужно создать новую структуру PyThreadState. В случае, если запущен один интерпретатор, можно воспользоваться API для такой миграции потока. Если интерпретаторов несколько, придётся делать это вручную. Наконец, примерно так же, как и каждый фрейм связан через указатель с предыдущим, состояния потоков объединены связным списком указателей PyThreadState *next.

Список структур потоков связан со структурой интерпретатора, в котором находятся потоки. Структура интерпретатора определена в ./Include.h/pystate.h: PyInterpreterState. Создаётся она при вызове функции Py_Initialize, которая инициализирует в процессе виртуальную машину Питона, или при вызове функции Py_NewInterpreter, в которой создаётся новая структура интерпретатора (в случае, если в процессе не один интерпретатор). Для лучшего понимания напомню, что Py_NewInterpreter возвращает не структуру интерпретатора, а структуру PyThreadState только что созданного потока для нового интерпретатора. Создавать новый интерпретатор без единого потока в нём не имеет особого смысла, так же, как и нет смысла в процессах без потоков в них. Структуры интерпретаторов в процессе связаны друг с другом так же, как и структуры потоков в интерпретаторе.

В целом, наше путешествие от единственной операции к целому процессу закончено: операции находятся в исполняющихся объектах кода (в то время как «неисполняющиеся» объекты лежат где-то рядом, как обычные данные), объекты кода находятся в исполняющихся фреймах, которые находятся в Python-потоках, а потоки в свою очередь принадлежат интерпретатору. На корень всей этой структуры ссылается статическая переменная ./Python/pystate.c: interp_head. Указывает она на структуру первого интерпретатора (через неё доступны все остальные интерпретаторы, потоки и т.д.). Мьютекс head_mutex защищает от повреждения этих структур конкурирующими изменениями из разных потоков (уточняю, что это не GIL, а обычный мьютекс для структур интерпретатора и потоков). Эта блокировка контролируется макросами HEAD_LOCK и HEAD_UNLOCK. Как правило, к переменной interp_head обращаются в случае, если нужно добавить новый или удалить существующий интерпретатор или поток. Если в процессе не один интерпретатор, то по этой переменной доступна структура не обязательно того интерпретатора, в котором находится исполняемый в текущий момент поток.

Надёжнее пользоваться переменной ./Python/pystate.c: _PyThreadState_Current, которая указывает на структуру исполняемого потока (при этом нужно учесть некоторые условия). То есть, чтобы добраться до своего интерпретатора, коду нужна структура его потока, из которой уже можно вытащить интерпретатор. Для доступа к этой переменной (взять текущее значение или сменить его, сохранив старое) есть функции, для работы которых необходимо удерживать GIL. Это важно, и это одна из тех проблем, которые возникают из-за отсутствия потокобезопасности в CPython. Значение переменной _PyThreadState_Current устанавливается в структуре нового потока во время инициализации Питона или во время создания нового потока. Когда Python-поток впервые запускается после начальной загрузки, он полагается на то, что: а) он удерживает GIL и б) значение переменной _PyThreadState_Current корректно. В этот момент поток не должен отдавать GIL. Сначала он должен сохранить куда-нибудь значение _PyThreadState_Current, чтобы при следующем захвате GIL можно было восстановить нужное значение переменной и продолжить работу. Благодаря такому поведению _PyThreadState_Current всегда указывает на выполняющийся в данный момент поток. Для реализации этого поведения есть макросы Py_BEGIN_ALLOW_THREADS и Py_END_ALLOW_THREADS. О GIL и API для работы с ним можно говорить часами, и было бы интересно сравнить CPython с другими реализациями (например, Jython или IronPython, в которых потоки выполняются одновременно). Но давайте пока отложим эту тему.

На схеме я указал связи между структурами одного процесса, в котором запущены два интерпретатора с двумя потоками у каждого, которые ссылаются на свои стеки фреймов.
image

Красиво, да? Так. Всё обсудили, но до сих пор непонятно, в чём смысл этих структур. Зачем они нужны? Что в них интересного? Я не хочу усложнять, поэтому только коротко расскажу о некоторых функциях. В структуре интерпретатора, например, есть поля, предназначенные для работы с импортируемыми модулями; указатели, необходимые для работы с юникодом; поле флагов динамического компоновщика и поле связанное с использованием TSC для профилирования (смотрите предпоследний пункт здесь).

Некоторые поля структуры потока связаны с деталями исполнения этого потока. Например, поля recursion_depth, overflow и recursion_critical нужны для того, чтобы поймать слишком глубокую рекурсию и выбросить исключение RuntimeError до того, как переполнится стек нижележащей платформы, и рухнет весь процесс. Там же находятся поля, связанные с профилированием, трассировкой и обработкой исключений и словарь для хранения всякого хлама.

Думаю, на этом рассказ о структуре Python-процесса можно закончить. Надеюсь, всё понятно. В следующих постах мы перейдём к настоящему хардкору и поговорим о фрейм-объектах, пространствах имён и объектах кода. Готовьтесь.
Метки:
Буруки 26,78
Компания
Поделиться публикацией
Похожие публикации
Комментарии 11
  • +3
    Оффтоп, извините, но — Python pystate!
    • +1
      pystate pysta pystateй pystatой тачки
      © Наггано
    • +1
      Спасибо за ваши труды по переводу!

      Недавно столкнулся с тем, что в Python освобождение ресурсов дочернего процесса не гарантируется до смерти родительского процесса. Надеялся узнать что-нибудь о природе этого явления из статьи, но, к сожалению, ситуация не прояснилась.
      • 0
        Это вопрос к сборщику мусора :)
        • 0
          Что-то вы дивное рассказываете.
          Можете привести подробности?
          • +1
            Python 2.7.3
            Ubuntu 12.04 LTS

            Задача: очень быстро разобрать 100 файлов размером по 50 мегабайт каждый.
            Что сделано:
            from multiprocessing import Pool, cpu_count
            pool = Pool(processes=cpu_count())
            result = pool.imap_unordered(parse_func, file_list)
            collect_data_from_result(result)
            

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

            Что происходит на самом деле:
            Освобождение памяти происходит лишь после завершения работы родительского процесса, в котором была вызвана обработка. Как и смерть дочерних процессов.

            Я вполне допускаю, что мои ожидания были напрасны. Если Вы вдруг знаете ссылку на официальные доки по этому поводу, подскажите — с удовольствием почитаю.
            • +2
              Ага, это не просто дочерние процессы а multiprocessing.Pool, что несколько меняет дело.
              pool.close()
              pool.join()
              

              делали?

              Без этого pool дочерние процессы не закрывает.
              Вот вам ссылка: docs.python.org/2/library/multiprocessing.html#multiprocessing.pool.multiprocessing.Pool.close
              • +1
                Без этого pool дочерние процессы не закрывает.

                Я почему-то искренне был уверен, что они закроются, когда не останется ссылок на pool. В этом было моё заблуждение.

                И, всё-таки, немного странно поведение. Если дочерние процессы выполнили все положенные им инструкции, а возможностей добавить новые нет, т.к. ссылок на pool не осталось, зачем держать эти процессы запущенными?
                • +2
                  Ссылка на pool остается в главном процессе.
                  Дочерние не закрываются, потому что главный может добавить в pool еще работу после того как дочерние отработали предудущие задания. С точки зрения дочернего процесса без дополнительных инструкций не понять, предполагает ли главный процесс как-то еще использовать pool или нет.
                  • +1
                    Спасибо за объяснения.
        • +9
          По картинке я понял что внутри Python находится PHP.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое