Что такое GIL в Python?
Кажется, один из золотых вопросов для всех питонистов на собеседованиях.
Обычно, на встречный вопрос "а что конкретно в питоне является GIL?" не может ответить ни один спрашивающий.
Сегодня мы закроем данный пробел в знаниях питонистов.
Global Interpreter Lock не позволяет более чем одному треду работать с Python API за раз. Его можно отключить через
Обратите внимание на ключевую фразу "c Python API". С системными треды могут и должны работать в режиме настоящей параллельности, без GIL. Что и позволяет получить ускорение при использовании
Знакомьтесь – вот структура GIL
Как можно отпустить GIL?
На уровне C есть макросы: Py_BEGIN_ALLOW_THREADS и Py_END_ALLOW_THREADS, которые отпускают GIL в нужных местах. Пример из модуля mmap:
Или time.sleep, который тоже дает работать другим тредам, пока ждет.
Что происходит, когда мы используем данный макрос? Они разворачиваются в:
Когда вызывается PyEval_SaveThread и GIL отпускается, то на самом деле мы просто помечаем текущий PyThreadState как:
И вызываем _PyEval_ReleaseLock, который уже правильно изменит
Как итог – текущий стейт теряет возможность вызывать какие-либо Python АПИ. Даже, например
Как треды берут GIL?
Смотрим на thread_run из
Там используется PyEval_AcquireThread, который берет GIL в конкретном треде для работы с Python API.
И дальше – отпускаем.
В следующих сериях поговорим про переключение тредов, ParkingLot API, Mutex'ы и прочее.
Обсуждение: сталкивались ли вы на собесах с вопросами про GIL? Стало ли теперь понятнее?
| Поддержать | YouTube | GitHub | Чат |
Кажется, один из золотых вопросов для всех питонистов на собеседованиях.
Обычно, на встречный вопрос "а что конкретно в питоне является GIL?" не может ответить ни один спрашивающий.
Сегодня мы закроем данный пробел в знаниях питонистов.
Global Interpreter Lock не позволяет более чем одному треду работать с Python API за раз. Его можно отключить через
--disable-gil в 3.13+, но сегодня мы про такое не будем.Обратите внимание на ключевую фразу "c Python API". С системными треды могут и должны работать в режиме настоящей параллельности, без GIL. Что и позволяет получить ускорение при использовании
threading, когда C код поддерживает такой способ.Знакомьтесь – вот структура GIL
_gil_runtime_state и поведение в ceval_gil.c.Как можно отпустить GIL?
На уровне C есть макросы: Py_BEGIN_ALLOW_THREADS и Py_END_ALLOW_THREADS, которые отпускают GIL в нужных местах. Пример из модуля mmap:
Py_BEGIN_ALLOW_THREADS
m_obj->data = mmap(NULL, map_size, prot, flags, fd, offset);
Py_END_ALLOW_THREADS
Или time.sleep, который тоже дает работать другим тредам, пока ждет.
Что происходит, когда мы используем данный макрос? Они разворачиваются в:
{
PyThreadState *_save;
_save = PyEval_SaveThread();
// your code here
PyEval_RestoreThread(_save);
}
PyThreadState является текущим состоянием треда в CPython. Внутри хранится много контекста. Нас особо сильно интересует часть с полями про GIL:
struct PyThreadState {
struct {
unsigned int initialized:1;
/* Has been bound to an OS thread. */
unsigned int bound:1;
/* Has been unbound from its OS thread. */
unsigned int unbound:1;
/* Has been bound aa current for the GILState API. */
unsigned int bound_gilstate:1;
/* Currently in use (maybe holds the GIL). */
unsigned int active:1;
/* Currently holds the GIL. */
unsigned int holds_gil:1;
} _status;
// Thread state (_Py_THREAD_ATTACHED, _Py_THREAD_DETACHED, _Py_THREAD_SUSPENDED).
int state;
// ...
}
Когда вызывается PyEval_SaveThread и GIL отпускается, то на самом деле мы просто помечаем текущий PyThreadState как:
tstate->_status.active = 0;
tstate->_status.unbound = 1;
tstate->_status.holds_gil = 0;
tstate->state = detached_state;
И вызываем _PyEval_ReleaseLock, который уже правильно изменит
_gil_runtime_state. Как итог – текущий стейт теряет возможность вызывать какие-либо Python АПИ. Даже, например
Py_DECREF, и в тредах есть свой refcount, который работает локально, чтобы можно было его вызывать без GIL.Как треды берут GIL?
Смотрим на thread_run из
_threadmodule.c.
_PyThreadState_Bind(tstate);
PyEval_AcquireThread(tstate);
_Py_atomic_add_ssize(&tstate->interp->threads.count, 1);
Там используется PyEval_AcquireThread, который берет GIL в конкретном треде для работы с Python API.
И дальше – отпускаем.
В следующих сериях поговорим про переключение тредов, ParkingLot API, Mutex'ы и прочее.
Обсуждение: сталкивались ли вы на собесах с вопросами про GIL? Стало ли теперь понятнее?
| Поддержать | YouTube | GitHub | Чат |
GitHub
cpython/Include/internal/pycore_gil.h at fb2d325725dcc881868b576b9d0d9f4bf7f24fe0 · python/cpython
The Python programming language. Contribute to python/cpython development by creating an account on GitHub.
63👍93🔥39❤16🤯8👌2🥰1🤡1
unraisable exceptions в питоне
Мы все с вами привыкли, что в питоне можно "зарайзить" исключение в любой момент:
Но, что если в какой-то момент времени мы не можем вызывать исключение?
Простейший пример: что произойдет при запуске такого скрипта?
Тут может быть два варианта:
1. Или
2. Или случится какая-то магия, ошибка будет вызвана, напечатается, но программа продолжится
Ну и так как мы с вами на том канале, где мы с вами, то конечно же будет второй вариант.
Знакомьтесь – unraisable exceptions 🤝
Как оно работает?
В некоторых местах C кода у нас есть необходимость вызывать исключения, но нет технической возможности. Пример, как выглядит упрощенный
А, как вы можете знать, чтобы в C коде вызвать ошибку, нужно сделать две вещи:
- Взывать специальное АПИ вроде
- И вернуть
Ошибку мы "вызываем" через специальные АПИ:
- PyErr_WriteUnraisable
- PyErr_FormatUnraisable
Они создают ошибку, но не выкидывают её обычным способом, а сразу отправляют в специальный хук-обработчик.
В питоне оно используется где-то 150 раз. То есть – прям часто. Примеры:
- Ошибки при завершении интерпретатора, попробуйте сами:
- Ошибки внутри sys.excepthook
- Ошибки внутри
- Ошибки внутри логики установки ошибок (вдруг память кончилась, например) 🌚️️️️
- И многое другое
Пользовательское АПИ
Ну и конечно же, есть специальный хук для обработки таких ошибок: sys.unraisablehook
Например, pytest использует кастомный хук, чтобы валить тесты при возникновении такой ситуации. Что логично.
Обсуждение: знали ли вы про такую особенность? Приходилось ли где-то в мониторинге особо настраивать?
| Поддержать | YouTube | GitHub | Чат |
Мы все с вами привыкли, что в питоне можно "зарайзить" исключение в любой момент:
raise ExceptionНо, что если в какой-то момент времени мы не можем вызывать исключение?
Простейший пример: что произойдет при запуске такого скрипта?
# ex.py
class BrokenDel:
def __del__(self):
raise ValueError('del is broken')
obj = BrokenDel()
del obj
print('done!') # будет ли выведено?
Тут может быть два варианта:
1. Или
del вызовет ValueError и программа завершится2. Или случится какая-то магия, ошибка будет вызвана, напечатается, но программа продолжится
Ну и так как мы с вами на том канале, где мы с вами, то конечно же будет второй вариант.
» ./python.exe ex.py
Exception ignored while calling deallocator <function BrokenDel.__del__ at 0x10303c1d0>:
Traceback (most recent call last):
File "/Users/sobolev/Desktop/cpython/ex.py", line 3, in __del__
raise ValueError('del is broken')
ValueError: del is broken
done!
Знакомьтесь – unraisable exceptions 🤝
Как оно работает?
В некоторых местах C кода у нас есть необходимость вызывать исключения, но нет технической возможности. Пример, как выглядит упрощенный
dealloc для list?
static void
list_dealloc(PyListObject *op)
{
Py_ssize_t i;
PyObject_GC_UnTrack(op); // убираем объект из отслеживания gc
if (op->ob_item != NULL) {
i = Py_SIZE(op);
while (--i >= 0) {
Py_XDECREF(op->ob_item[i]); // уменьшаем счетчик ссылок каждого объекта в списке
}
op->ob_item = NULL;
}
PyObject_GC_Del(op);
}
А, как вы можете знать, чтобы в C коде вызвать ошибку, нужно сделать две вещи:
- Взывать специальное АПИ вроде
PyErr_SetString(PyExc_ValueError, "some text")- И вернуть
NULL как PyObject * из соответствующих АПИ, показывая, что у нас ошибка. Если вернуть NULL нельзя, то мы не можем поставить ошибку. А тут у нас void и вернуть вообще ничего нельзя. Потому приходится использовать вот такой подход с unraisable exceptionОшибку мы "вызываем" через специальные АПИ:
- PyErr_WriteUnraisable
- PyErr_FormatUnraisable
Они создают ошибку, но не выкидывают её обычным способом, а сразу отправляют в специальный хук-обработчик.
В питоне оно используется где-то 150 раз. То есть – прям часто. Примеры:
- Ошибки при завершении интерпретатора, попробуйте сами:
import atexit
def foo():
raise Exception('foo')
atexit.register(foo)
- Ошибки внутри sys.excepthook
- Ошибки внутри
gc- Ошибки внутри логики установки ошибок (вдруг память кончилась, например) 🌚️️️️
- И многое другое
Пользовательское АПИ
Ну и конечно же, есть специальный хук для обработки таких ошибок: sys.unraisablehook
Например, pytest использует кастомный хук, чтобы валить тесты при возникновении такой ситуации. Что логично.
Обсуждение: знали ли вы про такую особенность? Приходилось ли где-то в мониторинге особо настраивать?
| Поддержать | YouTube | GitHub | Чат |
Python documentation
Exception Handling
The functions described in this chapter will let you handle and raise Python exceptions. It is important to understand some of the basics of Python exception handling. It works somewhat like the PO...
7🤯111🔥36👍28❤4👎1🤡1
Три типа объектов в Питоне
В питоне часто любят обсуждать "мутабельные" и "иммутабельные" объекты, но крайне редко объясняют, в чем же на самом деле разница. Сегодня мы посмотрим на такое со стороны C.
PyObject
Все мы знаем, что в питоне все объект или
То есть: у нас есть только счетчик ссылок на объект и указатель на его тип. Первое меняется очень часто, если объект не immortal. Второе тоже можно менять в некоторых случаях:
Получается, что большинство объектов мутабельные уже на данном уровне.
Но, в целом есть три типа объектов, разные по уровню мутабельности:
1. Такие как
2. Такие как
3. Такие как
Отдельно нужно отметить, что пользовательские классы обычно еще более мутабельны, потому что их тип менять можно.
Но, вопрос в другом: а где вообще хранится размер объекта и его внутренности? Раз в
C-API
В C-API питона есть два полезных макроса: PyObject_HEAD для объектов фиксированного размера и PyObject_VAR_HEAD для объектов, которые могут менять размер.
Хотим поменять размер объекта? Увеличиваем
Итоговые объекты используют примерно такую логику:
То есть: на самом деле все объекты
- Имеют свой собственный тип:
- Имеют общую абстракцию для размерности:
- Имеют общую абстракцию для типа и счетчика ссылок:
Я сделал небольшой очень упрощенный пример. Там я показываю в том числе, как происходит каст одного типа поинтера в другой в C.
Итог
1.
2.
3.
Обсуждение: как вы думаете, как работает
| Поддержать | YouTube | GitHub | Чат |
В питоне часто любят обсуждать "мутабельные" и "иммутабельные" объекты, но крайне редко объясняют, в чем же на самом деле разница. Сегодня мы посмотрим на такое со стороны C.
PyObject
Все мы знаем, что в питоне все объект или
PyObject *, который упрощенно выглядит так (в FT сборке он посложнее):
struct _object {
Py_ssize_t ob_refcnt;
PyTypeObject *ob_type;
}
То есть: у нас есть только счетчик ссылок на объект и указатель на его тип. Первое меняется очень часто, если объект не immortal. Второе тоже можно менять в некоторых случаях:
>>> class A:
... __slots__ = ()
>>> class B:
... __slots__ = ()
>>> a = A()
>>> type(a)
<class '__main__.A'>
>>> a.__class__ = B
>>> type(a)
<class '__main__.B'>
Получается, что большинство объектов мутабельные уже на данном уровне.
Но, в целом есть три типа объектов, разные по уровню мутабельности:
1. Такие как
None: ob_refcnt не меняется (immortal), тип менять нельзя, ведь Py_TPFLAGS_IMMUTABLETYPE установлен (static type), размер неизменный 0 для всех потенциальных значений2. Такие как
int: ob_refcnt может меняться для больших чисел (маленькие инты - immortal), тип менять нельзя, размер нельзя менять, но он будет разный для разных чисел:
>>> sys.getsizeof(1)
28
>>> sys.getsizeof(10000000000000)
32
3. Такие как
list: ob_refcnt всегда меняется, тип менять нельзя, размер меняетсяОтдельно нужно отметить, что пользовательские классы обычно еще более мутабельны, потому что их тип менять можно.
Но, вопрос в другом: а где вообще хранится размер объекта и его внутренности? Раз в
PyObject ничего такого нет.C-API
В C-API питона есть два полезных макроса: PyObject_HEAD для объектов фиксированного размера и PyObject_VAR_HEAD для объектов, которые могут менять размер.
struct PyVarObject {
PyObject ob_base;
Py_ssize_t ob_size; /* Number of items in variable part */
};
#define PyObject_HEAD PyObject ob_base;
#define PyObject_VAR_HEAD PyVarObject ob_base;
Хотим поменять размер объекта? Увеличиваем
ob_size, аллоцируем новую память (если нужно) под новые объекты внутри.Итоговые объекты используют примерно такую логику:
typedef struct {
PyObject_VAR_HEAD
/* Vector of pointers to list elements. list[0] is ob_item[0], etc. */
PyObject **ob_item;
/* ob_item contains space for 'allocated' elements. The number
* currently in use is ob_size.
*/
Py_ssize_t allocated;
} PyListObject;
То есть: на самом деле все объекты
list (и многие другие) не просто PyObject, они:- Имеют свой собственный тип:
PyListObject- Имеют общую абстракцию для размерности:
PyVarObject- Имеют общую абстракцию для типа и счетчика ссылок:
PyObjectЯ сделал небольшой очень упрощенный пример. Там я показываю в том числе, как происходит каст одного типа поинтера в другой в C.
Итог
1.
None не имеет внутреннего состояния вообще (не использует ничего)2.
int может иметь разный размер, но не может изменяться, потому использует PyObject_HEAD (раньше был PyObject_VAR_HEAD, там сложная история):
typedef struct _PyLongValue {
uintptr_t lv_tag; /* Number of digits, sign and flags */
digit ob_digit[1];
} _PyLongValue;
struct _longobject {
PyObject_HEAD
_PyLongValue long_value;
};
3.
list может иметь разный размер и может изменятся, потому использует PyObject_VAR_HEAD, как я показывал вышеОбсуждение: как вы думаете, как работает
len() для list? | Поддержать | YouTube | GitHub | Чат |
Python documentation
Common Object Structures
There are a large number of structures which are used in the definition of object types for Python. This section describes these structures and how they are used. Base object types and macros: All ...
6👍75❤22🔥4💩4🕊1
Находки в опенсорсе
Аллокаторы в СPython: PyArena Один из самых простых аллокаторов в питоне. Исходники. По сути данный аллокатор является небольшой оберткой поверх PyMem_Malloc, но с интересной особенностью. Если PyMem_Malloc имеет PyMem_Free для освобождения памяти каждого…
Аллокаторы в СPython: база
Тема аллокаторов иногда питонистам кажется сложной, потому что в питоне мы их не вызываем явно. Оттого с ними не очень знакомы, так давайте исправлять и знакомиться!
Зачем вообще нужно много разных аллокаторов? Все они делают одно и то же: выделяют память в куче (heap). В зависимости от наших вариантов использования данной памяти - выделять и освобождать её нужно очень по-разному.
Где-то множество мелких объектов, которые часто создаются и очищаются. Где-то несколько больших, которые должны умирать все вместе. Где-то мы работаем в рамках одного потока, где-то несколько потоков будут запрашивать / высвобождать память параллельно.
Например: при парсинге AST мы используем PyArena аллокатор. Он выделяет сразу много памяти, сразу вычищает все за один раз. Что идеально подходит для парсинга.
Но, для рантайма - задачи, конечно же другие. Там есть долгоживущие объекты, есть много мелких краткоживущих, есть довольно большие, есть маленькие. Для таких задач используют "general purpose allocators". Которые в среднем хороши во всем.
Дизайн аллокаторов в CPython
Питон знает, как его будут использовать. Потому поверх базовых GPA есть собственные надстройки.
Документация:
- https://docs.python.org/3/c-api/allocation.html
- https://docs.python.org/3/c-api/memory.html
В CPython есть: malloc, pymalloc, mimalloc и некоторые их варианты для дебага.
Они разделены на три "домена" для аллокаторов, то с чем они работают, какие задачи решают:
-
-
-
Разработчики C-extensions должны понимать, когда какой использовать и под какие задачи.
К счастью, разработчикам на питоне - такое нужно только для любопытства.
А вот таблица, какие реальные аллокаторы используют те или иные C-API функции в разных режимах:
Она правда немного устарела и не отражает Free-Threading сборки, которые требуют mimalloc 🌚
Кто первый успеет сделать PR с исправлением - тот молодец!
О
Зачем питону свой аллокатор?
В CPython есть (был? для free-threading он не используется и не будет) свой аллокатор: pymalloc, основная задача которого – работа с маленькими Python объектами.
Про него полностью тоже нужно писать большой отдельный пост.
Что вообще важно в аллокаторе?
- Стратегия выделения памяти под новый запрос
- Работа с округлениями размера памяти и выравнивание
- Дефрагментация памяти
- Стратегия очистки памяти
Но кратко про
- Он создает арены по 1MB
- Внутри арены разделены на пулы по 16KB
- Внутри пулы поделены на блоки фиксированного размера
Зачем? Чтобы не аллоцировать часто маленькие кусочки памяти. Что дорого.
Можно ли управлять аллокаторами?
Да! Есть опции для сборки:
И даже переменная окружения PYTHONMALLOC, которая позволяет указать, какой аллокатор использовать для всех случаев. Зачем? Прежде всего для дебага. Но можно потестить, вдруг будет давать буст по скорости или потреблению памяти в ваших вариантах использования.
Обсуждение: какой ваш любимый аллокатор? И почему jemalloc?
| Поддержать | YouTube | GitHub | Чат |
Тема аллокаторов иногда питонистам кажется сложной, потому что в питоне мы их не вызываем явно. Оттого с ними не очень знакомы, так давайте исправлять и знакомиться!
Зачем вообще нужно много разных аллокаторов? Все они делают одно и то же: выделяют память в куче (heap). В зависимости от наших вариантов использования данной памяти - выделять и освобождать её нужно очень по-разному.
Где-то множество мелких объектов, которые часто создаются и очищаются. Где-то несколько больших, которые должны умирать все вместе. Где-то мы работаем в рамках одного потока, где-то несколько потоков будут запрашивать / высвобождать память параллельно.
Например: при парсинге AST мы используем PyArena аллокатор. Он выделяет сразу много памяти, сразу вычищает все за один раз. Что идеально подходит для парсинга.
Но, для рантайма - задачи, конечно же другие. Там есть долгоживущие объекты, есть много мелких краткоживущих, есть довольно большие, есть маленькие. Для таких задач используют "general purpose allocators". Которые в среднем хороши во всем.
Дизайн аллокаторов в CPython
Питон знает, как его будут использовать. Потому поверх базовых GPA есть собственные надстройки.
Документация:
- https://docs.python.org/3/c-api/allocation.html
- https://docs.python.org/3/c-api/memory.html
В CPython есть: malloc, pymalloc, mimalloc и некоторые их варианты для дебага.
Они разделены на три "домена" для аллокаторов, то с чем они работают, какие задачи решают:
-
Raw: для выделения памяти для общих задач, например под сишные буферы или IO. Может работать без PyThreadState-
Mem: для выделения памяти для общих задач, но уже с PyThreadState, например под Python буферы, подходит для мелких объектов-
Object: для выделения памяти под конкретные мелкие объектыРазработчики C-extensions должны понимать, когда какой использовать и под какие задачи.
К счастью, разработчикам на питоне - такое нужно только для любопытства.
А вот таблица, какие реальные аллокаторы используют те или иные C-API функции в разных режимах:
PyMem_RawMalloc -> malloc
PyMem_Malloc -> pymalloc
PyObject_Malloc -> pymalloc
Она правда немного устарела и не отражает Free-Threading сборки, которые требуют mimalloc 🌚
Кто первый успеет сделать PR с исправлением - тот молодец!
О
mimalloc мы как-нибудь отдельно поговорим, там нужно рассказывать сильно глубже, в том числе про GC и PyGC_Head.Зачем питону свой аллокатор?
В CPython есть (был? для free-threading он не используется и не будет) свой аллокатор: pymalloc, основная задача которого – работа с маленькими Python объектами.
Про него полностью тоже нужно писать большой отдельный пост.
Что вообще важно в аллокаторе?
- Стратегия выделения памяти под новый запрос
- Работа с округлениями размера памяти и выравнивание
- Дефрагментация памяти
- Стратегия очистки памяти
struct arena_object {
uintptr_t address;
pymem_block* pool_address;
uint nfreepools;
uint ntotalpools;
struct pool_header* freepools;
struct arena_object* nextarena;
struct arena_object* prevarena;
};
Но кратко про
pymalloc можно сказать следующее:- Он создает арены по 1MB
- Внутри арены разделены на пулы по 16KB
- Внутри пулы поделены на блоки фиксированного размера
Зачем? Чтобы не аллоцировать часто маленькие кусочки памяти. Что дорого.
Можно ли управлять аллокаторами?
Да! Есть опции для сборки:
--without-mimalloc, --without-pymallocИ даже переменная окружения PYTHONMALLOC, которая позволяет указать, какой аллокатор использовать для всех случаев. Зачем? Прежде всего для дебага. Но можно потестить, вдруг будет давать буст по скорости или потреблению памяти в ваших вариантах использования.
Обсуждение: какой ваш любимый аллокатор? И почему jemalloc?
| Поддержать | YouTube | GitHub | Чат |
Python documentation
Allocating Objects on the Heap
Deprecated aliases: These are soft deprecated aliases to existing functions and macros. They exist solely for backwards compatibility.,, Deprecated alias, Function,,,, PyObject_New,,, PyObject_NewV...
🔥45👍20❤7🕊1