Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Внутри CPython гид по интерпретатору Python.pdf
Скачиваний:
6
Добавлен:
07.04.2024
Размер:
8.59 Mб
Скачать

242    Параллелизм и конкурентность

значение PTHREAD_SYSTEM_SCHED_SUPPORTED, то поток pthread переводится в режим PTHREAD_SCOPE_SYSTEM; это означает, что приоритет потока в планировщике ОС определяется относительно других потоков в системе, а не только внутри процесса Python.

После того как свойства потоков будут настроены, создается поток вызовом функции pthread_create(). При этом из нового потока будет запущена функция инициализации.

Наконец, дескриптор потока pthread_t преобразуется в unsigned long и возвращается, превращаясь в идентификатор потока.

Потоки Windows

Потоки Windows, реализованные в Python thread_nt.h, используют похожую, но более простую схему.

Размер стека нового потока настраивается в соответствии со значением интерпретатора pythread_stacksize (если оно задано). Затем создается поток функцией Windows API beginthreadex(), которая использует функцию загрузки (bootstrap) в качестве обратного вызова. Схема завершается возвращением идентификатора потока.

Многопоточность: выводы

Этот раздел не является исчерпывающим руководством по использованию потоков Python. Реализация потоков в Python весьма обширна, и она предоставляет множество механизмов для совместного использования данных потоками, блокировки объектов и ресурсов.

Потоки — эффективный способ повышения производительности приложений Python с интенсивным вводом/выводом. В этом разделе вы узнали, что такое блокировка GIL, зачем она нужна и какие части стандартной библиотеки могут быть выведены из-под ее ограничений.

АСИНХРОННОЕ ПРОГРАММИРОВАНИЕ

Python предоставляет разные способы реализации параллельного программирования без использования потоков и многопроцессной обработки. Эти

Книги для программистов: https://t.me/booksforits

Генераторы    243

возможности неоднократно дополнялись, расширялись и часто заменялись более совершенными альтернативами.

Для использующейся в этой книге версии (3.9) декоратор @coroutine считается устаревшим.

Все еще доступно:

zz Создание объектов future (futures) с ключевыми словами async.

zz Выполнение сопрограмм с использованием конструкции yield from.

ГЕНЕРАТОРЫ

Генераторы Python представляют собой функции, которые возвращают значение с помощью оператора yield и при последующих обращениях генерируют новые значения.

Генераторы часто используются как механизм перебора значений в большом блоке данных (файле, базе данных, по сети), более эффективно использующий память. Когда используется yield, а не return, вместо значений возвращаются объекты генераторов. Объект генератора создается оператором yield и возвращается на сторону вызова.

Простая функция-генератор, перебирающая буквы от a до z:

cpython-book-samples 33 letter_generator.py

def letters():

i = 97 # Letter 'a' in ASCII

end = 97 + 26 # Буква 'z' в ASCII while i < end:

yield chr(i) i += 1

Если вызвать функцию letters(), она не вернет значения. Вместо этого она вернет объект генератора:

>>>from letter_generator import letters

>>>letters()

<generator object letters at 0x1004d39b0>

В синтаксис оператора for встроена возможность перебора через объект генератора до тех пор, пока он не перестанет возвращать значения:

Книги для программистов: https://t.me/booksforits

244    Параллелизм и конкурентность

>>> for letter in letters():

... print(letter) a

b c d

...

В этой реализации используется протокол итераторов. Объекты, содержащие метод __next__(), можно перебирать в циклах for и while или с использованием встроенного метода next().

Все контейнерные типы (списки, множества, кортежи) в Python реализуют протокол итераторов. Генераторы уникальны тем, что выполнение метода __next__() заново вызывает функцию-генератор из ее последнего состояния.

Генераторы не выполняются в фоновом режиме — они приостанавливаются. Когда вы запрашиваете следующее значение, они возобновляют выполнение. В структуре объекта генератора хранится объект кадра в том виде, в котором он находился при выполнении последней команды yield.

Структура генератора

Объекты генераторов создаются шаблонным макросом _PyGenObject_ HEAD(prefix).

Этот макрос используется следующими типами и префиксами:

zz Объекты генераторов: PyGenObject (gi_) zz Объекты сопрограмм: PyCoroObject (cr_)

zz Асинхронные объекты генераторов: PyAsyncGenObject (ag_)

Объекты сопрограмм и асинхронных генераторов будут рассмотрены далее.

Тип PyGenObject содержит следующие базовые свойства.

ПОЛЕ

ТИП

НАЗНАЧЕНИЕ

[x]_code

PyObject *

Скомпилированная функция, которая возвра-

 

(PyCodeObject*)

щает генератор оператором yield

[x]_exc_state

_PyErr_StackItem

Данные исключения (если оно поднимается

 

 

при вызове генератора)

Книги для программистов: https://t.me/booksforits

 

 

Генераторы    245

 

 

 

ПОЛЕ

ТИП

НАЗНАЧЕНИЕ

[x]_frame

PyFrameObject*

Текущий объект кадра для генератора

[x]_name

PyObject * (str)

Имя генератора

[x]_qualname

PyObject * (str)

Уточненное имя генератора

[x]_running

char

0 или 1, в зависимости от того, работает ли

 

 

генератор в настоящее время

[x]_weakreflist

PyObject * (list)

Список слабых ссылок на объекты внутри

 

 

функции-генератора

Кроме базовых свойств, тип PyCoroObject содержит следующее свойство:

ПОЛЕ

ТИП

НАЗНАЧЕНИЕ

cr_origin

PyObject * (tuple)

Кортеж, содержащий кадр, в котором создан

 

 

генератор, и сторону вызова

Кроме базовых свойств, тип PyAsyncGenObject содержит следующие свойства.

ПОЛЕ

ТИП

НАЗНАЧЕНИЕ

ag_closed

int

Флаг для пометки того, что генератор закрыт

ag_finalizer

PyObject *

Ссылка на метод-финализатор

ag_hooks_inited

int

Флаг для пометки того, что хуки инициализиро-

 

 

ваны

ag_running_

int

Флаг для пометки того, что генератор выполня-

asyn

 

ется

Исходные файлы

Ниже перечислены исходные файлы, относящиеся к генераторам.

ФАЙЛ НАЗНАЧЕНИЕ

Include genobject.h

API генераторов и определение PyGenObject

Objects genobject.c Реализация объекта генератора

Книги для программистов: https://t.me/booksforits

246    Параллелизм и конкурентность

Создание генераторов

При компиляции функции, содержащей оператор yield, в итоговый объект кода включается дополнительный флаг CO_GENERATOR.

В разделе «Построение объектов кадров» главы «Цикл вычисления» было показано, как скомпилированный объект кода при выполнении преобразуется в объект кадра. В этом процессе выделяется особый вариант для генераторов, сопрограмм и асинхронных генераторов.

_PyEval_EvalCode() проверяет объект кода на наличие флагов CO_GENERATOR, CO_COROUTINE и CO_ASYNC_GENERATOR. Если один из этих флагов будет найден, то вместо встроенного вычисления объекта кода функция создает кадр и преобразует его в объект генератора, сопрограммы или асинхронного генератора вызовом

PyGen_NewWithQualName(), PyCoro_New() или PyAsyncGen_New() соответственно:

PyObject *

_PyEval_EvalCode(PyObject *_co, PyObject *globals, PyObject *locals, ...

...

 

/* Обработка генератора/сопрограммы/асинхронного генератора */

 

if (co->co_flags & (CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR)) {

 

PyObject *gen;

 

PyObject *coro_wrapper = tstate->coroutine_wrapper;

 

int is_coro = co->co_flags & CO_COROUTINE;

 

...

 

/* Создает новый генератор, который является владельцем кадра,

 

* готового к выполнению, и возвращает его как значение */

 

if (is_coro) {

>>>

gen = PyCoro_New(f, name, qualname);

}else if (co->co_flags & CO_ASYNC_GENERATOR) {

>>>gen = PyAsyncGen_New(f, name, qualname);

}else {

>>>gen = PyGen_NewWithQualName(f, name, qualname);

}

...

return gen;

}

...

Фабрика генераторов PyGen_NewWithQualName() получает кадр и для заполнения полей объекта генератора выполняет следующие действия:

1.Свойству gi_code присваивается скомпилированный объект кода.

2.Генератор переводится в нерабочее состояние (gi_running = 0).

3.Спискам исключений и слабых ссылок присваивается NULL.

Книги для программистов: https://t.me/booksforits

Генераторы    247

Чтобы убедиться в том, что gi_code является скомпилированным объектом кода для функций-генераторов, импортируйте модуль dis и дизассемблируйте его байт-код:

>>>from letter_generator import letters

>>>gen = letters()

>>>import dis

>>>dis.disco(gen.gi_code)

20 LOAD_CONST 1 (97)

2 STORE_FAST 0 (i)

...

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

Встроенное поведение и состояние объектов кадров позволяет генераторам приостанавливать и возобновлять выполнение по требованию.

Выполнение генераторов

Каждый раз, когда для объекта генератора вызывается __next__(), с экземпляром генератора вызывается функция gen_iternext(), что немедленно приводит к вызову gen_send_ex() из Objects genobject.c.

Функция gen_send_ex() преобразует объект генератора в следующий результат, возвращаемый yield. Можно заметить много общего с построением кадров из объекта кода, так как эти функции выполняют сходные задачи. gen_send_ex() используется для стандартных генераторов, сопрограмм и асинхронных генераторов и работает по следующей схеме:

1.Читается текущее состояние потока.

2.Читается объект кадра из объекта генератора.

3.Если генератор запускается при вызове __next__(), выдается исключение ValueError.

4.Если кадр в генераторе находится на вершине стека, возможны три варианта:

Если это сопрограмма, еще не помеченная как закрытая, выдается

RuntimeError.

Если это асинхронный генератор, выдается исключение StopAsyncIte­ ration.

Книги для программистов: https://t.me/booksforits

248    Параллелизм и конкурентность

Если это стандартный генератор, поднимается исключение StopIte­ ration.

5.Если последняя инструкция в кадре (f->f_lasti) все еще содержит -1, потому что кадр только что запустился, и если это сопрограмма или асинхронный генератор, то в аргументе не может передаваться никакое другое значение, кроме None, и выдается исключение.

6.В противном случае это первый вызов, и аргументы разрешены. Значение аргумента заносится в стек значений кадра.

7.Поле f_back кадра содержит сторону вызова, которой передаются возвращаемые значения, поэтому полю присваивается текущий кадр в потоке. Это означает, что возвращаемое значение будет отправляться на сторону вызова, а не туда, где генератор создан.

8.Генератор помечается как работающий.

9.Последнее исключение в информации об исключениях генератора копируется из последнего исключения состояния потока.

10.Информация об исключении состояния потока записывается в адрес информации об исключениях генератора. Это означает, что если сторона вызова попадет в точку останова в момент выполнения генератора, трассировка стека пройдет через генератор, и мы увидим код, вызвавший ошибку.

11.Кадр внутри генератора выполняется в главном цикле Python ceval.c, после чего возвращается значение.

12.Информация последнего исключения состояния потока сбрасывается до значения, предшествующего вызову кадра.

13.Генератор помечается как не работающий.

14.В зависимости от возвращаемого значения может быть несколько вариантов исключений при вызове генератора. Помните, что генераторы должны выдавать исключение StopIteration при исчерпании данных — либо вручную, либо не возвращая значение:

Если кадр ничего не возвращает, то для стандартных генераторов выдается исключение StopIteration, а для асинхронных —

StopAsyncIteration.

Если исключение StopIteration было выдано явно, но это сопрограмма или асинхронный генератор, то выдается RuntimeError, так как такая ситуация недопустима.

Книги для программистов: https://t.me/booksforits