Что такое gil python
Зачем нужен Python Global Interpreter Lock и как он работает
Python Global Interpreter Lock (GIL) — это своеобразная блокировка, позволяющая только одному потоку управлять интерпретатором Python. Это означает, что в любой момент времени будет выполняться только один конкретный поток.
Работа GIL может казаться несущественной для разработчиков, создающих однопоточные программы. Но во многопоточных программах отсутствие GIL может негативно сказываться на производительности процессоро-зависымых программ.
Поскольку GIL позволяет работать только одному потоку даже в многопоточном приложении, он заработал репутацию «печально известной» функции.
В этой статье будет рассказано о том, как GIL влияет на производительность приложений, и о том, как это самое влияние можно смягчить.
Что за проблему в Python решает GIL?
Python подсчитывает количество ссылок для корректного управления памятью. Это означает, что созданные в Python объекты имеют переменную подсчёта ссылок, в которой хранится количество всех ссылок на этот объект. Как только эта переменная становится равной нулю, память, выделенная под этот объект, освобождается.
Вот небольшой пример кода, демонстрирующий работу переменных подсчёта ссылок:
Проблема, которую решает GIL, связана с тем, что в многопоточном приложении сразу несколько потоков могут увеличивать или уменьшать значения этого счётчика ссылок. Это может привести к тому, что память очистится неправильно и удалится тот объект, на который ещё существует ссылка.
Счётчик ссылок можно защитить, добавив блокираторы на все структуры данных, которые распространяются по нескольким потокам. В таком случае счётчик будет изменяться исключительно последовательно.
Но добавление блокировки к нескольким объектам может привести к появлению другой проблемы — взаимоблокировки (англ. deadlocks), которая получается только если блокировка есть более чем на одном объекте. К тому же эта проблема тоже снижала бы производительность из-за многократной установки блокираторов.
GIL — эта одиночный блокиратор самого интерпретатора Python. Он добавляет правило: любое выполнение байткода в Python требует блокировки интерпретатора. В таком случае можно исключить взаимоблокировку, т. к. GIL будет единственной блокировкой в приложении. К тому же его влияние на производительность процессора совсем не критично. Однако стоит помнить, что GIL уверенно делает любую программу однопоточной.
Несмотря на то, что GIL используется и в других интерпретаторах, например в Ruby, он не является единственным решением этой проблемы. Некоторые языки решают проблему потокобезопасного освобождения памяти с помощью сборки мусора.
С другой стороны это означает, что такие языки часто должны компенсировать потерю однопоточных преимуществ GIL добавлением каких-то дополнительных функций повышения производительности, например JIT-компиляторов.
Почему для решения проблемы был выбран именно GIL?
Итак, почему же это не очень «хорошее» решение используется в Python? Насколько для разработчиков это решение критично?
По словам Larry Hastings, архитектурное решение GIL — это одна из тех вещей, которые сделали Python популярным.
Python существует с тех времён, когда в операционных системах не существовало понятия о потоках. Этот язык разрабатывался в расчёте на лёгкое использование и ускорение процесса разработки. Всё больше и больше разработчиков переходило на Python.
Много расширений, в которых нуждался Python, было написано для уже существующих библиотек на C. Для предотвращения несогласованных изменений, язык C требовал потокобезопасного управления памятью, которое смог предоставить GIL.
GIL можно было легко реализовать и интегрировать в Python. Он увеличивал производительность однопоточных приложений, поскольку управление велось только одним блокиратором.
Те библиотеки на C, которые не были потокобезопасными, стало легче интегрировать. Эти расширения на C стали одной из причин, почему Python-сообщество стало расширяться.
Как можно понять, GIL — фактическое решение проблемы, с которой столкнулись разработчики CPython в начале жизни Python.
Влияние GIL на многопоточные приложения
Если смотреть на типичную программу (не обязательно написанную на Python) — есть разница, ограничена ли эта программа производительностью процессора или же I/O.
Операции, ограниченные производительностью процессора (англ. CPU-bound) — это все вычислительные операции: перемножение матриц, поиск, обработка изображений и т. д.
Операции, ограниченные производительностью I/O (англ. I/O-bound) — это те операции, которые часто находятся в ожидании чего-либо от источников ввода/вывода (пользователь, файл, БД, сеть). Такие программы и операции иногда могут ждать долгое время, пока не получат от источника то, что им нужно. Это связано с тем, что источник может проводить собственные (внутренние) операции, прежде чем он будет готов выдать результат. Например, пользователь может думать над тем, что именно ввести в поисковую строку или же какой запрос отправить в БД.
Ниже приведена простая CPU-bound программа, которая попросту ведёт обратный отсчёт:
Запустив это на 4х-ядерном компьютере получим такой результат:
Ниже приведена та же программа, с небольшим изменением. Теперь обратный отсчёт ведётся в двух параллельных потоках:
Как видно из результатов, оба варианта затратили примерно одинаковое время. В многопоточной версии GIL предотвратил параллельное выполнение потоков.
GIL не сильно влияет на производительность I/O-операций в многопоточных программах, т. к. в процессе ожидания от I/O блокировка распространяется по потокам.
Однако программа, потоки которой будут работать исключительно с процессором (например обработка изображения по частям), из-за блокировки не только станет однопоточной, но и на её выполнение будет затрачиваться больше времени, чем если бы она изначально была строго однопоточной.
Такое увеличение времени — это результат появления и реализации блокировки.
Почему GIL всё ещё используют?
Разработчики языка получили уйму жалоб касательно GIL. Но такой популярный язык как Python не может провести такое радикальное изменение, как удаление GIL, ведь это, естественно, повлечёт за собой кучу проблем несовместимости.
В прошлом разработчиками были предприняты попытки удаления GIL. Но все эти попытки разрушались существующими расширениями на C, которые плотно зависели от существующих GIL-решений. Естественно, есть и другие варианты, схожие с GIL. Однако они либо снижают производительность однопоточных и многопоточных I/O-приложений, либо попросту сложны в реализации. Вам бы не хотелось, чтобы в новых версиях ваша программа работала медленней, чем сейчас, ведь так?
Создатель Python, Guido van Rossum, в сентябре 2007 года высказался по поводу этого в статье «It isn’t Easy to remove the GIL»:
«Я был бы рад патчам в Py3k только в том случае, если бы производительность однопоточных приложений или многопоточных I/O-приложений не уменьшалась.»
С тех пор ни одна из предпринятых попыток не удовлетворяла это условие.
Почему GIL не был удалён в Python 3?
Python 3 на самом деле имел возможность переделки некоторых функций с нуля, хотя из-за этого многие расширения на С попросту сломались бы и их пришлось бы переделывать. Именно из-за этого первые версии Python 3 так слабо расходились по сообществу.
Но почему бы параллельно с обновлением Python 3 не удалить GIL?
Его удаление сделает однопоточность в Python 3 медленней по сравнению с Python 2 и просто представьте, во что это выльется. Нельзя не заметить преимущества однопоточности в GIL. Именно поэтому он всё ещё не удалён.
Но в Python 3 действительно появились улучшения для существующего GIL. До этого момента в статье рассказывалось о влиянии GIL на многопоточные программы, которые затрагивают только процессор или только I/O. А что насчёт тех программ, у которых часть потоков идут на процессор, а часть на I/O?
В таких программах I/O-потоки «страдают» из-за того, что у них нет доступа к GIL от процессорных потоков. Это связано со встроенным в Python механизмом, который принуждал потоки освобождать GIL после определённого интервала непрерывного использования. В случае, если никто другой не используют GIL, эти потоки могли продолжать работу.
Но тут есть одна проблема. Почти всегда GIL занимается процессорными потоками и остальные потоки не успевают занять место. Этот факт был изучен David Beazley, визуализацию этого можно увидеть здесь.
Проблема была решена в Python 3.2 в 2009 разработчиком Antoine Pitrou. Он добавил механизм подсчёта потоков, которые нуждаются в GIL. И если есть другие потоки, нуждающиеся в GIL, текущий поток не занимал бы их место.
Как справиться GIL?
Если GIL у вас вызывает проблемы, вот несколько решений, которые вы можете попробовать:
После запуска получаем такой результат:
Можно заметить приличное повышение производительности по сравнению с многопоточной версией. Однако показатель времени не снизился до половины. Всё из-за того, что управление процессами само по себе сказывается на производительности. Несколько процессов более сложны, чем несколько потоков, поэтому с ними нужно работать аккуратно.
Альтернативные интерпретаторы Python. У Python есть много разных реализаций интерпретаторов. CPython, Jyton, IronPython и PyPy, написанные на C, Java, C# и Python соответственно. GIL существует только на оригинальном интерпретаторе — на CPython.
Вы просто можете использовать преимущества однопоточности, в то время, пока одни из самых ярких умов прямо сейчас работают над устранением GIL из CPython. Вот одна из попыток.
Зачастую, GIL рассматривается как нечто-то сложное и непонятное. Но имейте ввиду, что как python-разработчик, вы столкнётесь с GIL только если будете писать расширения на C или многопоточные процессорные программы.
На этом этапе вы должны понимать все аспекты, необходимые при работе с GIL. Если же вам интересна низкоуровневая структура GIL — посмотрите Understanding the Python GIL от David Beazley.
И еще раз о GIL в Python
Предисловие
Область, в которой мне повезло работать, называется вычислительная электрофизиология сердца. Физиология сердечной деятельности определяется электрическими процессами, происходящими на уровне отдельных клеток миокарда. Эти электрические процессы создают электрическое поле, которое достаточно легко измерить. Более того оно очень неплохо описывается в рамках математических моделей электростатики. Тут и возникает уникальная возможность строго математически описать работу сердца, а значит — и усовершенствовать методы лечения многих сердечных заболеваний.
За время работы в этой области у меня накопился некоторый опыт использования различных вычислительных технологий. На некоторые вопросы, которые могут быть интересны не только мне, я постараюсь отвечать в рамках этой публикации.
Кратко о Scientific Python
Начиная еще с первых курсов университета, я пытался найти идеальный инструмент для быстрой разработки численных алгоритмов. Если отбросить ряд откровенно маргинальных технологий, я курсировал между C++ и MATLAB. Это продолжалось до тех пор, пока я не открыл для себя Scientific Python [1].
Scientific Python представляет собой набор библиотек языка Python для научных вычислений и научной визуализации. В своей работе я использую следующие пакеты, которые покрывают примерно 90% моих потребностей:
Название | Описание |
---|---|
NumPy | Одна из базовых библиотек, позволяет работать с многомерными массивами как с едиными объектами в MATLAB стиле. Включает реализацию основных процедур линейной алгебры, преобразование Фурье, работу со случайными числами и др. |
SciPy | Расширение NumPy, включает реализацию методов оптимизации, работу с разряженными матрицами, статистику и др. |
Pandas | Отдельный пакет для анализа многомерных данных и статистики. |
SymPy | Пакет символьной математики. |
Matplotlib | Двумерная графика. |
Mayavi2 | Трехмерная графика на основе VTK. |
Spyder | Удобная IDE для интерактивной разработки математических алгоритмов. |
В Scientific Python я нашел для себя великолепный баланс между удобной высокоуровневой абстракцией для быстрой разработки численных алгоритмов и современным развитым языком. Но, как известно, не бывает идеальных инструментов. И одна из достаточно критических проблем в Python — это проблема параллельных вычислений.
Проблемы параллельных вычислений в Python.
Под параллельными вычислениями в этой статье я буду понимать SMP — симметричный мультипроцессинг с общей памятью. Вопросов использования CUDA и систем с раздельной памятью (чаще всего используется стандарт MPI) касаться не буду.
Проблема заключается в GIL. GIL (Global Interpreter Lock) — это блокировка (mutex), которая не позволяет нескольким потокам выполнить один и тот же байткод. Эта блокировка, к сожалению, является необходимой, так как система управления памятью в CPython не является потокобезопасной. Да, GIL это не проблема языка Python, а проблема реализации интерпретатора CPython. Но, к сожалению, остальные реализации Python не слишком приспособлены для создания быстрых численных алгоритмов.
К счастью, в настоящее время существует несколько способов решения проблем GIL. Рассмотрим их.
Тестовая задача
Даны два набора по N векторов: P=
1,p2,…,pN> и Q=1,q2,…,qN>
в трехмерном евклидовом пространстве. Необходимо построить матрицу R размерностью N x N, каждый элемент ri,j которой вычисляется по формуле:
Грубо говоря, нужно вычислить матрицу, использующую попарные расстояния между всеми векторами. Эта матрица достаточно часто используется в реальных расчетах, например, при RBF интерполяции или решении дифуров в чп методом интегральных уравнений.
В тестовых экспериментах количество векторов N = 5000. Для вычислений использовался процессор с 4 ядрами. Результаты получены по среднему времени из 10 запусков.
Полную реализацию тестовых задач можно поглядеть на GitHub [2].
Правильное замечание в комментариях от «@chersaya». Данная тестовая задача используется здесь в качестве примера. Если нужно действительно вычислить попарные расстояния, правильнее использовать функцию scipy.spatial.distance.cdist.
Параллельная реализация на C++
Для сравнения эффективности параллельных вычислений на Python, я реализовал эту задачу на C++. Код основной функции выглядит следующий образом.
Что здесь интересного? Ну прежде всего я использовал отдельный класс Vector3D для представления вектора в трехмерном пространстве. Перегруженный оператор «*» в этом классе имеет смысл скалярного произведения. Для представления набора векторов я использовал std::vector. Для параллельных вычислений использовалась технология OpenMP. Для параллелизации алгоритма достаточно использовать директиву «#pragma omp parallel for».
Результаты:
Однопроцессорный С++ | 224 ms |
Многопроцессорный C++ | 65 ms |
Ускорение в 3.45 раза при параллельном расчете я считаю вполне неплохим для четырехядерного процессора.
Параллельные реализации на Python
1.Наивная реализация на чистом Python
В этом тесте хотелось проверить сколько будет решаться задача на чистом Python без использования каких-либо специальных пакетов.
Здесь p, q – входные данные в формате NumPy массивов размерностями (N, 3) и (3, N). А дальше идет честный цикл на Python, вычисляющий элементы матрицы R.
Результаты:
Однопроцессорный Python | 57 386 ms |
Да, да, именно 57 тысяч миллисекунд. Где-то в 256 раз медленнее однопроцессорного C++. В общем, это совсем не вариант для численных расчетов.
2 Однопроцессорный NumPy
Вообще, для вычислений на Python с использованием NumPy иногда можно вообще не задумываться о параллельности. Так, например, процедура умножения двух матриц на NumPy будет в итоге все-равно выполняться с использованием низкоуровневых высокоэффективных библиотек линейной алгебры на C++ (MKL или ATLAS). Но, к сожалению, это верно лишь для наиболее типовых операций и не работает в общем случае. Наша тестовая задача, к сожалению, будет выполняться последовательно.
Код решения следующий:
Всего 4 строчки и никаких циклов! Вот за это я и люблю NumPy.
Результаты:
Однопроцессорный NumPy | 973 ms |
Примерно в 4.3 раза медленнее однопроцессорного C++. Вот это уже совсем неплохой результат. Для подавляющего большинства расчетов этой производительности вполне хватает. Но это все пока однопроцессорные результаты. Идем дальше к мультипроцессингу.
3 Многопроцессорный NumPy
В качестве решения проблем с GIL традиционно предлагается использовать несколько независимых процессов выполнения вместо нескольких потоков выполнения. Все бы хорошо, но есть проблема. Каждый процесс обладает независимой памятью, и нам необходимо в каждый процесс передавать матрицу результатов. Для решения этой проблемы в Python multiprocessing вводится класс RawArray, предоставляющий возможность разделить один массив данных между процессами. Не знаю точно, что лежит в основе RawArray. Мне кажется, что это memory mapped files.
Код решения следующий:
Мы создаем разделенные массивы для входных данных и выходной матрицы, создаем пул процессов по числу ядер, разбиваем задачу на подзадачи и решаем параллельно.
Результаты:
Многопроцессорный NumPy | 795 ms |
Да, быстрее однопроцессорного варианта, но всего в 1.22 раза. С ростом числа N эффективность решения растет. Но, в целом и общем, наша тестовая задача не слишком приспособлена для решения в рамках множества независимых процессов с независимой памятью. Хотя для других задач такой вариант может быть вполне эффективным.
На этом известные мне решения для параллельного программирования с использование только Python закончились. Далее, как бы нам не хотелось, для освобождения от GIL придется спускаться на уровень C++. Но этот не так страшно, как кажется.
4 Cython
Cython [3] — это расширение языка Python, позволяющее внедрять инструкции на языке C в код на Python. Таким образом, мы можем взять код на Python и добавлением нескольких инструкций значительно ускорить узкие в плане производительности места. Cython модули преобразуются в код на C и далее компилируются в Python модули. Код решения нашей задачи на Cython следующий:
Если сравнить данный код с реализацией на чистом Python, то все, что нам пришлось сделать, это всего лишь указать типы для используемых переменных. GIL отпускается одной строчкой. Параллельный цикл организуется всего лишь инструкцией prange вместо xrange. На мой взгляд, вполне несложно и красиво!
Результаты:
Однопроцессорный Cython | 255 ms |
Многопроцессорный Cython | 75 ms |
Вау! Время исполнения почти совпадает с временем исполнения на C++. Отставание примерно в 1.1 раз как в однопроцессорном, так и в многопроцессорном вариантах практически незаметно на реальных задачах.
5 Numba
Numba [4] достаточно новая библиотека, находится в активном развитии. Идея здесь примерно такая же, что и в Cython — попытка спуститься на уровень C++ в коде на Python. Но идея реализована существенно элегантнее.
Numba основана на LLVM компиляторах, которые позволяют производить компиляцию непосредственно в процессе исполнения программы (JIT компиляция). Например, для компилирования любой процедуры на Python достаточно всего лишь добавить аннотацию «jit». Более того аннотации позволяют указывать типы входных/выходных данных, что делает JIT-компиляцию существенно более эффективной.
Код реализации задачи следующий.
По сравнению с чистым Python, к однопроцессорному решению на Numba добавляется всего одна аннотация! Многопроцессорный вариант, к сожалению, не так красив. В нем требуется организовывать пул потоков, в ручном режиме отдавать GIL. В предыдущих релизах Numba была попытка реализовать параллельных цикл одной инструкцией, но из-за проблем стабильности в последующих релизах эта возможность была убрана. Я уверен, что с течением времени эту возможность починят.
Результаты выполнения:
Однопроцессорный Numba | 359 ms |
Многопроцессорный Numba | 180 ms |
Слегка хуже, чем Cython, но результаты все-равно очень достойные! А само решение крайне элегантное.
Выводы
Результаты я хочу проиллюстрировать следующими диаграммами:
Рис. 1. Результаты однопроцессорных вычислений
Рис. 2. Результаты многопроцессорных вычислений
Мне кажется, что проблемы GIL в Python для численных расчетов практически преодолены. Пока в качестве технологии параллельных вычислений я бы рекомендовал Cython. Но очень бы внимательно пригляделся бы к Numba.
Как устроен GIL в Python
Почему после распараллеливания выполнение вашей программы может замедлиться вдвое?
Почему после создания потока перестает работать Ctrl-C?
Представляю вашему вниманию перевод статьи David Beazley «Inside the Python GIL». В ней рассматриваются некоторые тонкости работы потоков и обработки сигналов в Python.
Вступление
Как известно, в Python используется глобальная блокировка интерпретатора (Global Interpreter Lock — GIL), накладывающая некоторые ограничения на потоки. А именно, нельзя использовать несколько процессоров одновременно. Это избитая тема для холиваров о Python, наряду с tail-call оптимизацией, lambda, whitespace и т. д.
Дисклеймер
Я не испытываю глубокого возмущения по поводу использования GIL в Python. Но для параллельных вычислений с использованием нескольких CPU я предпочитаю передачу сообщений и межпроцессное взимодействие использованию потоков. Однако меня интересует неожиданное поведение GIL на многоядерных процессорах.
Тест производительности
Рассмотрим тривиальную CPU-зависимую функцию (т.е. функцию, скорость выполнения которой зависит преимущественно от производительности процессора):
Сначала запустим ее дважды по очереди:
Теперь запустим ее параллельно в двух потоках:
Подробнее о потоках
Python threads — это настоящие потоки (POSIX threads или Windows threads), полностью контролируемые ОС. Рассмотрим поточное выполнение в процессе интерпретатора Python (написанного на C). При создании поток просто выполняет метод run() объекта Thread или любую заданную функцию:
На самом деле происходит гораздо большее. Python создает маленькую структуру данных (PyThreadState), в которой указаны: текущий stack frame в коде Python, текущая глубина рекурсии, идентификатор потока, некоторая информация об исключениях. Структура занимает менее 100 байт. Затем запускается новый поток (pthread), в котором код на языке C вызывает PyEval_CallObject, который запускает то, что указано в Python callable.
Интерпретатор хранит в глобальной переменной указатель на текущий активный поток. Выполняемые действия всецело зависят от этой переменной:
Печально известный GIL
В этом вся загвоздка: в любой момент может выполняться только один поток Python. Глобальная блокировка интерпретатора — GIL — тщательно контролирует выполнение тредов. GIL гарантирует каждому потоку эксклюзивный доступ к переменным интерпретатора (и соответствующие вызовы C-расширений работают правильно).
Принцип работы прост. Потоки удерживают GIL, пока выполняются. Однако они освобождают его при блокировании для операций ввода-вывода. Каждый раз, когда поток вынужден ждать, другие, готовые к выполнению, потоки используют свой шанс запуститься.
При работе с CPU-зависимыми потоками, которые никогда не производят операции ввода-вывода, интерпретатор периодически проводит проверку («the periodic check»).
По умолчанию это происходит каждые 100 «тиков», но этот параметр можно изменить с помощью sys.setcheckinterval(). Интервал проверки — глобальный счетчик, абсолютно независимый от порядка переключения потоков.
При периодической проверке в главном потоке запускаются обработчики сигналов, если таковые имеются. Затем GIL отключается и включается вновь. На этом этапе обеспечивается возможность переключения нескольких CPU-зависимых потоков (при кратком освобождении GIL другие треды имеют шанс на запуск).
Тики примерно соответствуют выполнению инструкций интерпретатора. Они не основываются на времени. Фактически, длинная операция может заблокировать всё:
Тики нельзя прервать, Ctrl-C в данном случае не остановит выполнение программы.
Сигналы
Когда поступает сигнал, интерпретатор запускает «check» после каждого тика, пока не запустится главный поток. Так как обработчики сигналов могут быть запущены только в главном потоке, интерпретатор часто выключает и включает GIL, пока не запустится главный поток.
Планировщик потоков
У Python нет средств для определения, какой поток должен запуститься следующим. Нет приоритетов, вытесняющей многозадачности, round-robin и т.п. Эта функция целиком возлагается на операционную систему. Это одна из причин странной работы сигналов: интерпретатор никак не может контроллировать запуск потоков, он просто переключает их как можно чаще, надеясь, что запустится главный поток.
Ctrl-C часто не срабатывает в многопоточных программах, потому что главный поток обычно заблокирован непрерываемым thread-join или lock. Пока он заблокирован, он не сможет запуститься. Как следствие, он не сможет выполнить обработчик сигнала.
В качестве дополнительного бонуса, интерпретатор остается в состоянии, где он пытается переключить поток после каждого тика. Мало того, что вы не можете прервать программу, она еще и работает медленнее.
Реализация GIL
Задержка между отправкой сигнала и запуском потока может быть довольно существенной, это зависит от операционной системы. А она учитывает приоритет выполнения. При этом задачи, требующие выполнения операций ввода-вывода, имеют более высокий приоритет, чем CPU-зависимые. Если сигнал посылается потоку с низким приоритетом, а процессор занят более важными задачами, то этот поток не будет выполняться довольно долго.
В результате сигналов, которые посылает поток GIL, становится слишком много.
Каждые 100 тиков интерпретатор блокирует мьютекс, посылает сигнал в переменную или семафор процессу, который всё время этого ждет.
Измерим количество системных вызовов.
Для последовательного выполнения: 736 (Unix), 117 (Mac).
Для двух потоков: 1149 (Unix), 3,3 млн. (Mac).
Для двух потоков на двухъядерной системе: 1149 (Unix), 9,5 млн. (Mac).
На многоядерной системе CPU-зависимые процессы переключаются одновременно (на разных ядрах), в результате происходит борьба за GIL:
Ожидающий поток при этом может сделать сотни безуспешных попыток захватить GIL.
Мы видим, что происходит битва за две взаимоисключающие цели. Python просто хочет запускать не больше одного потока в один момент. А операционная система («Ооо, много ядер!») щедро переключает потоки, пытаясь извлечь максимальную выгоду из всех ядер.
Даже один CPU-зависимый поток порождает проблемы — он увеличивает время отклика I/O-зависимого потока.
Последний пример — причудливая форма проблемы смены приоритетов. CPU-зависимый процесс (с низким приоритетом) блокирует выполнение I/O-зависимого (с высоким приоритетом). Это происходит только на многоядерных процессорах, потому что I/O-поток не может проснуться достаточно быстро и заполучить GIL раньше CPU-зависимого.
Заключение
Реализация GIL в Python за последние 10 лет почти не изменилась. Соответствующий код в Python 1.5.2 выглядит практически так же, как в Python 3.0. Я не знаю, было ли поведение GIL достаточно хорошо изучено (особенно на многоядерных процессорах). Полезнее удалить GIL вообще, чем изменять его. Мне кажется, этот предмет требует дальнейшего изучения. Если GIL остается с нами, стоит исправить его поведение.
Как же всё-таки избавиться от этой проблемы? У меня есть несколько смутных идей, но все они «сложные». Нужно, чтобы в Python появился свой собственный диспетчер потоков (или хотя бы механизм взаимодействовия с диспетчером ОС). Но это требует нетривиального взаимодействия между интерпретатором, планировщиком ОС, библиотекой потоков и, что самое страшное, модулями C-расширений.
Стоит ли оно того? Исправление поведения GIL сделало бы выполнение потоков (даже с GIL) более предсказуемым и менее требовательным к ресурсам. Возможно, улучшится производительность и уменьшится время отклика приложений. Надеюсь, при этом удастся избежать полного переписывания интерпретатора.
Послесловие от переводчика
Оригинал был оформлен как презентация, поэтому мне пришлось немного изменить порядок повествования, чтобы статью было легче читать. Также я исключил трассировки работы интерпретатора — если вам интересно, посмотрите в оригинале.
Хабралюди, посоветуйте интересные английские статьи по Python, которые было бы хорошо перевести. У меня есть на примете пара статей, но хочется еще вариантов.