Why you need Python Global Interpreter Lock and how it works
The Python Global Interpreter Lock (GIL) is a kind of lock that allows only one thread to control the Python interpreter. This means that only one particular thread will be running at any given time.
The operation of the GIL may seem irrelevant to developers creating single-threaded programs. But in multi-threaded programs, the absence of the GIL can negatively affect the performance of processor-dependent programs.
Because the GIL only allows one thread to run, even in a multi-threaded application, it has earned a reputation as an «infamous» feature.
This article will talk about how the GIL affects the performance of applications, and how this very impact can be mitigated.
Contents
What problem does the GIL solve in Python?
Python counts the number of references for correct memory management. This means that objects created in Python have a reference count variable that stores the number of all references to that object. As soon as this variable becomes equal to zero, the memory allocated for this object is freed.
Here is a small code example demonstrating how reference counting variables work:
In this example, the number of references to an empty array is 3. This array is referenced by variable a, variable b, and the argument passed to sys.getrefcount().
The problem that the GIL solves is that in a multi-threaded application, multiple threads can increment or decrement this reference count at the same time. This can lead to the fact that the memory is cleaned up incorrectly and the object to which the reference still exists is deleted.
The reference count can be secured by adding blockers on all data structures that are propagated across multiple threads. In this case, the counter will change exclusively sequentially.
But adding a lock to multiple objects can lead to another problem — deadlocks, which only happen if there is a lock on more than one object. In addition, this problem would also reduce performance due to the repeated installation of blockers.
GIL is a single blocker of the Python interpreter itself. It adds the rule that any execution of bytecode in Python requires an interpreter lock. In this case, the deadlock can be avoided because the GIL will be the only lock in the application. In addition, its impact on processor performance is not critical at all. However, it is worth remembering that the GIL confidently makes any program single-threaded.
Although the GIL is used in other interpreters, such as Ruby, it is not the only solution to this problem. Some languages solve the problem of thread-safe memory deallocation using garbage collection.
On the other hand, this means that such languages often have to compensate for the loss of the single-threaded benefits of the GIL by adding some additional performance enhancement features, such as JIT compilers.
Why was GIL chosen to solve the problem?
So why is this solution being used in Python? How critical is this decision for developers?
According to Larry Hastings, the GIL architecture is one of the things that made Python popular.
Python has been around since the days when operating systems didn’t have the concept of threads. This language was designed to be easy to use and speed up the development process. More and more developers switched to Python.
Many of the extensions that Python needed were written for existing C libraries. To prevent inconsistent changes, the C language required thread-safe memory management, which the GIL was able to provide.
The GIL could be easily implemented and integrated into Python. It increased the performance of single-threaded applications because only one blocker was in control.
Those C libraries that were not thread-safe became easier to integrate. These C extensions are one of the reasons why the Python community has grown.
As you can see, the GIL is the actual solution to the problem that the CPython developers faced early in Python’s life.
The impact of the GIL on multithreaded applications
Looking at a typical program (not necessarily written in Python) it makes a difference whether that program is limited by CPU performance or I/O.
CPU-bound operations are all computational operations: matrix multiplication, search, image processing, etc.
I/O performance-bound operations (I/O-bound) are those operations that are often waiting for something from I/O sources (user, file, database, network). Such programs and operations can sometimes wait a long time until they get what they need from the source. This is because the source may be doing its own (internal) operations before it is ready to produce a result. For example, the user can think about what exactly to enter in the search string or what query to send to the database.
Below is a simple CPU-bound program that simply counts down:
Now the countdown is carried out in two parallel streams:
In the multithreaded version, the GIL prevented threads from running in parallel.
The GIL does not greatly affect the performance of I/O operations in multi-threaded programs, since the lock is propagated through the threads while waiting for I/O.
However, a program whose threads will work exclusively with the processor will not only become single-threaded due to blocking, but it will also take more time to execute it than if it were originally strictly single-threaded.
This increase in time is the result of the appearance and implementation of blocking.
How to deal with GIL?
If the GIL is causing you problems, here are some solutions you can try:
Multiprocessing versus multithreading. A fairly popular solution, since each Python process has its own interpreter with memory allocated for it, so there will be no problems with the GIL. Python already has a multiprocessing module that makes it easy to create processes like this:
You can notice a decent performance improvement compared to the multi-threaded version. However, the time indicator did not drop to half. This is due to the fact that process management itself affects performance. Multiple processes are more complex than multiple threads, so they need to be handled with care.
Often, the GIL is seen as something complicated and incomprehensible. But keep in mind that as a python developer, you will only encounter the GIL if you write C extensions or multi-threaded CPU programs.
At this point, you should understand all the aspects required when working with the GIL. If you are interested in the low-level structure of the GIL, check out Understanding the Python GIL by David Beazley.
Name already in use
articles / threading / GIL.md
- Go to file T
- Go to line L
- Copy path
- Copy permalink
- Open with Desktop
- View raw
- Copy raw contents Copy raw contents
Copy raw contents
Copy raw contents
Title: Загадочный GIL Labels: python, gil, threads slug: gil
Наверное, каждый питонист слышал про существование Global Interpreter Lock (GIL). Обычно знание предмета исчерпывается фразой: «Это — та самая гадость, которая не позволяет запустить одновременно как минимум два потока, задействовав все имеющиеся ядра современного процессора».
Высказывание отчасти верное, но совершенно неконструктивное и не покрывающее всей многогранности рассматриваемого вопроса.
Позвольте мне пройтись по теме более подробно, рассмотрев вопросы GIL и реализации Питоном многопоточности с разных сторон.
Прежде всего GIL — это блокировка, которая обязательно должна быть взята перед любым обращением к Питону (а это не только исполнение питоновского кода а еще и вызовы Python C API). Строго говоря, единственные вызовы, доступные после запуска интерпретатора при незахваченном GIL — это его захват. Нарушение правила ведет к мгновенному аварийному завершению (лучший вариант) или отложенному краху программы (куда более худший и труднее отлаживаемый сценарий).
Многопоточный код на питоне
Это — самый простой уровень. Имеем обычную программу, состоящую исключительно из питоновских модулей ( .py файлы) и не содержащую Python C Extensions. Пусть в ней работают два потока: главный и запущенный нами.
Многопоточная программа на С не должна как-то отдельно регистрировать свои потоки — достаточно вызова API (pthread_create или CreateThread) для запуска потока. Интерпретатор питона для своей работы требует ряда структур. Давайте рассмотрим их подробнее.
Структуры интерпретатора, обеспечивающие многопоточную работу
PyInterpreterState содержит глобальное состояние интерпретатора: загруженные модули modules , указатель на первый (он же главный) поток tstate_head и кучу других важных для внутренней кухни вещей.
PyThreadState позволяет узнать, какой кадр стека ( frame ) исполняется и какой номер у потока с точки зрения операционной системы. Остальные атрибуты сейчас не важны.
PyFrameObject — это объект кадра стека. Питоновский объект, в отличие от первых двух структур (на это указывает PyObject_VAR_HEAD ). Имеет указатель на предыдущий кадр f_back , исполняемый код f_code и последнюю выполненную в этом коде инструкцию f_lasti , указатель на свой поток f_tstate и серию из глобального, локального и встроенного пространства имен ( f_globals , f_locals и f_builtins соответственно).
На самом деле членов в этих структурах поболее, и сами структуры отличаются от версии к версии (особенно заметны отличия между 2.x и 3.x) — но сейчас это не важно.
Важно понимать, что все три необходимых для исполнения структуры взаимно связаны между собой и PyThreadState_GET() возвращает указатель на текущий работающий поток:

Теперь пришло время показать, как именно работает GIL. Тут есть одна тонкость: в 3.2 его реализация довольно значительно изменилась. Для начала рассмотрим «старый» GIL, используемый в Python 2.x и 3.0/3.1.
GIL переключается каждые 100 инструкций. Под инструкцией здесь понимается одна операция питоновского байткода. Возьмем простую функцию:
Применим к ней дизассемблер:
Как видите, код превратился в последовательность этих самых инструкций, исполняемых интерпретатором. Скажу сразу: никакой внятной документации по байт-коду нет, инструкции добавляются и изменяются от версии к версии. Интересующиеся должны читать файл Python/ceval.c как первоисточник для понимания того, что какая инструкция делает.
Возвращаясь к GIL: он будет производить переключения между инструкциями, на каждой сотой выполненной.
Сам GIL устроен как обычная не-рекурсивная блокировка. Эта же структура лежит в основе threading.Lock . Реализуется через событие CreateEvent с бубенцами на Windows и семафор sem_t на Linux.
Давайте посмотрим на кусочек исходного кода функции PyEval_EvalFrameEx , которая представляет собой цикл с очень объемным switch/case внутри, исполняющим по одной инструкции за проход.
Как видите, все просто: имея захваченный GIL (а поток уже им владеет перед вызовом PyEval_EvalFrameEx ), каждый раз уменьшаем счетчик пока не дойдем до нуля. interpreter_lock — это наш GIL, указатель на объект блокировки. Если он есть (а есть всегда, за исключением специальных сборок питона с полностью отключенной многопоточностью), то происходит так называемое «переключение GIL».
PyThreadState_Swap сбрасывает указатель на текущий исполняемый поток (тот самый, который возвращается PyThreadState_GET ) и освобождает GIL.
Затем следующей строкой пытается захватить этот GIL снова. Хитрость в том, что если работает несколько потоков одновременно, то операционная система сама будет определять, какой поток из ожидающих в PyThread_acquire_lock получит эту блокировку (остальные будут ждать следующего освобождения interpreter_lock ). Современные операционные системы используют довольно замысловатые алгоритмы переключения потоков. Нам же нужно знать лишь то, что эти алгоритмы пытаются распределить время «справедливо», дав каждому поработать. Это означает, что только что освободивший GIL поток скорее всего обратно сразу же его не получит — а отдаст управление другому потоку и сам встанет в ожидание PyThread_acquire_lock .
Все работает, и схема получается надежная. Но она имеет ряд существенных недостатков:
- GIL переключается даже в однопоточной программе. Формально interpreter_lock создается не сразу при старте интерпретатора. Но импорт модуля threading или, к примеру, sqlite3 создаст GIL даже без создания второго потока. На практике правильней считать, что GIL есть всегда.
- Другими словами GIL переключается постоянно, независимо от того требует ли другой поток переключения или они все заблокированы ожиданием ввода-вывода или объектами синхронизации.
- Потоки «соревнуются» за захват GIL. Планировщик операционной системы — очень сложно устроенная штука. Поток, интенсивно использующий операции ввода-вывода, получает более высокий приоритет чем чисто вычислительный поток. Например, первый поток читает из файла и складывает прочитанное в очередь. Второй поток получает данные из очереди и обрабатывает их. Штука в том, что считывающий поток, обладая высоким приоритетом, может класть новые данные в очередь довольно долго, прежде чем обработчик получит возможность их обрабатывать. Да, первый поток регулярно освобождает GIL — но он тут же получает его назад (приоритет выше). Эта ситуация может быть исправлена выбором правильного способа взаимодействия между потоками, но решение зачастую неочевидно и, главное, проблема трудно локализуется.
- И, наконец, главная причина. Переключение происходит по количеству выполненных инструкций. Дело в том, что время выполнения инструкций может сильно отличаться (сравните простое сложение и создание списка на миллион элементов).
Для управления порогом переключения существуют функции:
- sys.getcheckinterval()
- sys.setcheckinterval(count)
В силу слабой связанности интервала переключения со временем исполнения эти функции практически бесполезны. По крайней мере я никогда не видел их применения в реальном коде.
Он использует усовершенствованную схему, базирующуюся на времени. Кроме того, добавлен специальный механизм для предотвращения повторного захвата GIL.
Снова выдержка из PyEval_EvalFrameEx , на этот раз Python 3.2.
Как видите, внешне почти ничего не изменилось. Ушел счетчик _Py_Ticker , Появились две переменные: eval_breaker и gil_drop_request . Переключение произойдет, если обе установлены (ненулевые). Две переменные нужны потому, что один и тот же механизм используется для штатного переключения GIL и для обработки сигналов операционной системы.
eval_breaker указывает на необходимость переключения, а gil_drop_request используется для штатной ситуации переключения потоков.
_Py_atomic_load_relaxed — это просто макрос для атомарного чтения переменной.
Вся магия скрыта внутри функций drop_gil и take_gil , работающих в паре.
Наш герой теперь называется gil_locked — обычная целочисленная переменная. Используется блокировка gil_mutex в паре с условной переменной gil_cond для синхронизации доступа к GIL. gil_drop_request — запрос на переключение GIL, защищенный switch_mutex и switch_cond .
В «отпускающей» стороне нет ничего сложного: прикрываясь gil_mutex сбрасываем GIL ( gil_locked ) в нолик и сигналим об этом событии через gil_cond .
Затем смотрим, это была просьба о переключении от другого потока или наш поток сам попросил освободиться. Дело в том, что Питон отпускает GIL перед системными вызовами. Чтение из файла, к примеру, может занимать длительное время и совершенно не требует GIL — можно дать шанс другим потокам поработать.
Если GIL освобождается не по внешнему запросу — работа закончена. Иначе нужно дождаться, пока попросивший не захватит GIL. Таким образом форсируется переключение на другой поток.
Захват GIL зеркально отражает его освобождение. Сначала ждем, пока GIL не освободится. Если ждем долго (больше 5 мс по умолчанию) и при этом не произошло переключения (не важно, на нас или какой другой поток) — выставляем запрос на переключение.
Дождавшись наконец свободного GIL, захватываем его и сигналим отдавшему потоку что передача состоялась. Естественно, все обращения защищены блокировками.
Что получилось в итоге:
- поток, владеющий GIL, не отдает его пока об этом не попросят.
- если уж отдал по просьбе, то подождет окончания переключения и не будет сразу же пытаться захватить GIL назад.
- поток, у которого сразу не получилось захватить GIL, сначала выждет 5 мс и лишь потом пошлет запрос на переключение, принуждая текущего владельца освободить ценный ресурс. Таким образом переключение осуществляется не чаще чем раз в 5 мс, если только владелец не отдаст GIL добровольно перед выполнением системного вызова.
Управление временем переключения — через sys.getswitchinterval и sys.setswitchinterval . Обратите внимание: в python 3.2 остались sys.getcheckinterval и sys.setcheckinterval , но они ни на что не влияют.
GIL и системные вызовы
Почти любое обращение к ядру операционной системы — довольно затратная операция. Более того, этот вызов может блокировать поток на значительный промежуток времени. Скажем, открытие файла может потребовать нескольких обращений к диску, если только этот файл уже не находится в файловом кэше.
GIL — один на всю программу. Слишком расточительно позволять потоку ждать окончания системного вызова (или любой другой операции, занимающей время и не требующей обращения к питоньим структурам), когда другие потоки простаивают в ожидании своей очереди на исполнение.
Поэтому перед вызовом такого долгоиграющего кода нужно отпустить GIL, а потом сразу же его захватить обратно:
Макросы Py_BEGIN_ALLOW_THREADS и Py_END_ALLOW_THREADS делают всю необходимую работу.
Обратите внимание, вкладывать друг в друга эти макросы запрещено! Один раз разрешив потоки, нельзя разрешить их вторично. Например, такой код ошибочный:
Если очень хочется, то внутри функции можно писать Py_BLOCK_THREADS / Py_UNBLOCK_THREADS для временного получения GIL назад. Например, так:
Во вложенной функции испрользовать Py_BLOCK_THREADS не получится — эти макросы используют стандартные питоновские вызовы PyEval_SaveThread / PyEval_RestoreThread с сохранением структуры PyThreadState в локальной переменной _save .
Коротко говоря, следите за руками и одновременно изучайте исходники Питона. Они занятные — регулярно перечитываю перед сном.
Вернемся опять к самому первому кусочку кода из этой статьи, который создавал поток из Питона. Питон — умный, он сам делает всю черновую работу, необходимую для регистрации потока в своих внутренних структурах.
Давайте посмотрим, как именно создается новый поток.
Просто хранит функцию, которую нужно выполнить в новом потоке, и ее параметры. Состояние потока и интерпретатора тоже пригодится.
Код, запускающий поток:
Ничего сложного: создаем bootstate и запускаем в новом потоке функцию t_bootstrap , которая должна закончить регистрацию PyThreadState . PyThread_start_new_thread — платформонезависимая обертка для создания потока ядра.
Сама запускаемая функция:
Код длинный, но простой. Из запущенного потока можно узнать его номер (идентификатор, используемый при обращениях к операционной системе). Осталось закончить инициализацию PyThreadState и выполнить запрашиваемую питоновскую функцию ( PyEval_CallObjectWithKeywords ). После окончания работы нужно почистить за собой. Если было исключение — записать его в stderr (исключения из потока не пробрасываются запустившему этот поток коду, остается только запись в поток ошибок).
Без приведенного пролога обращение к питону из нового потока приведет к краху интерпретатора. Причем не сразу же, а когда потребуется переключить GIL. Обращение к глобальным структурам может разрушить память.
Способ, используемый Питоном, рабочий. Но не самый подходящий для стороннего кода, желающего запускать питон в произвольном потоке, созданном без помощи Python API. Дело в том, что приведенные функции используют закрытую часть API — функции, начинающиеся с подчеркивания.
Код можно было бы переписать, полностью перенеся создание PyThreadState в t_bootstrap и сократив его до:
Вообще-то в наборе функций для работы с потоками и GIL наблюдается некоторый разброд и шатание. Например, PyEval_AcquireThread захватывает GIL. PyEval_RestoreThread делает практически то же самое плюс специальную проверку на случай завершения интерпретатора (которая в правильно написанной программе не нужна, порожденные потоки должны получить сигнал о завершении раньше, чем произойдет завершение питоновского кода в главном потоке). То же самое можно сказать про пару PyEval_ReleaseThread и PyEval_SaveThread и так далее.
В оправдание сложившегося положения вещей можно сказать, что эта часть API развивалась долго, постепенно переходя из закрытой в публичную и документированную. Существующие сторонние модули чаще всего писались по принципу «работает—и ладно», использование закрытого API авторов не волновало (тем более что открытое API появлялось с некоторым опозданием). На данный момент имеем множество библиотек, которые могут поломаться, вздумай разработчики Питона разом отрубить все устаревшие части API. Поэтому процесс удаления устаревшего кода занимает как минимум 2-3 версии питона (что составляет около 4-5 лет) со строгими предупреждениями и разъяснениями. И тем не менее всегда остаются недовольные авторы, чей код «внезапно» перестал компилироваться.
Временное получение GIL
Бывает так, что нужно выполнить питоновский вызов, не зная — зарегистрирован ли поток или еще нет. Для этого существует пара PyGILState_Ensure и PyGILState_Release :
Между вызовами ensure/release можно делать всё, что угодно — GIL захвачен, PyThreadState настроен. Можно, например, использовать Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS и вообще вызывать любой питоновский код. Более того, можно делать вложенные вызовы PyGILState_Ensure — главное не забывать о необходимых PyGILState_Release .
Нужно только всегда помнить об одной маленькой детали. Дело в том, что при каждом вызове PyGILState_Ensure система смотрит, был ли зарегистрирован PyThreadState для исполняемого потока. Если был — то захват происходит быстро. Иначе нужно создать и зарегистрировать новый PyThreadState . Для существующей структуры достаточно просто увеличить счетчик использования. PyGILState_Release этот счетчик уменьшает и, досчитав до нуля, удаляет зарегистрированный PyThreadState . На удаление тоже нужно время. Потери относительно небольшие, если только код не исполняется очень много раз. Иными словами, вместо создания/удаления PyThreadState в цикле:
лучше написать работающий практически так же, но более оптимальный по скорости код:
Я постарался как можно подробней описать, как работает GIL. Полностью покрыть эту тему невозможно даже в большой статье. Поэтому, если возникают вопросы — задавайте их или (что надежней и полезней) читайте ответ в исходном коде Питона, он простой и понятный.
Несколько слов «на общие темы».
GIL не уберут никогда. Или, по крайней мере, в ближайший десяток лет. Сейчас никаких работ на эту тему не ведется. Если некий гений предъявит работающую реализацию без GIL, ничего не ломающую и работающую не медленней, чем существующая версия — предложению будет открыт зеленый свет. Пока же «убрать GIL» проходит по части благих, но невыполнимых пожеланий.
В Java и C# никакого GIL нет. Потому что у них иначе устроен garbage collector. Если хотите, он более прогрессивный. Переделать GC Питона, не сломав обратной совместимости со всеми существующими библиотеками, использующими Python C API — невозможно. Сообщество и так уже который год лихорадит в связи с переходом на Python 3.x. Разработчики не желают выкатывать второе революционное изменение, не разобравшись с первым. Ждите Python 4.x (которого нет даже в планах) — до тех пор ничего не поменяется.
Несмотря на то, что GIL позволяет работать только одному питоновскому потоку на запущенный процесс, существуют способы нагрузить все имеющиеся ядра процессора.
- Во первых, если поток не делает вызовов Python C API — то GIL ему не нужен. Так можно держать много параллельно работающих потоков-числодробилок плюс несколько медленных питоновских потоков для управления всем хозяйством. Конечно, для этого нужно уметь писать Python C Extensions.
- Второй способ еще лучше. Замените «поток» на «процесс». По настоящему высоконагруженная система в любом случае должна строится с учетом масштабируемости и высокой надежности. На эту тему можно говорить очень долго, но хорошая архитектура автоматически позволяет вам запускать несколько процессов на одной машине, которые общаются между собой через какую-либо систему сообщений. В качестве одного из приятных бонусов получается избавление от «проклятия GIL» — у каждого процесса он только один, но процессов много!
Надеюсь, прочитанное поможет вам понять, как GIL работает, какие у него есть неочевидные особенности. И, главное — запомнить, как он не работает никогда!
GIL и обработка сигналов
В дополнение к этой статье рекомендую прочесть про особенности обработки сигналов Питоном в posix средах.
Что такое gil в python
In CPython, the global interpreter lock, or GIL, is a mutex that protects access to Python objects, preventing multiple threads from executing Python bytecodes at once. The GIL prevents race conditions and ensures thread safety. A nice explanation of how the Python GIL helps in these areas can be found here. In short, this mutex is necessary mainly because CPython’s memory management is not thread-safe.
In hindsight, the GIL is not ideal, since it prevents multithreaded CPython programs from taking full advantage of multiprocessor systems in certain situations. Luckily, many potentially blocking or long-running operations, such as I/O, image processing, and NumPy number crunching, happen outside the GIL. Therefore it is only in multithreaded programs that spend a lot of time inside the GIL, interpreting CPython bytecode, that the GIL becomes a bottleneck.
Unfortunately, since the GIL exists, other features have grown to depend on the guarantees that it enforces. This makes it hard to remove the GIL without breaking many official and unofficial Python packages and modules.
The GIL can degrade performance even when it is not a bottleneck. Summarizing the linked slides: The system call overhead is significant, especially on multicore hardware. Two threads calling a function may take twice as much time as a single thread calling the function twice. The GIL can cause I/O-bound threads to be scheduled ahead of CPU-bound threads, and it prevents signals from being delivered.
CPython extensions must be GIL-aware in order to avoid defeating threads. For an explanation, see Global interpreter lock.
Non-CPython implementations
[Mention place of GIL in StacklessPython.]
Eliminating the GIL
-
The language doesn’t say anything about what sort of atomicity any operation has. That dict operations are often thread-safe in CPython today is mostly to avoid low-level crashes.
-
I’d say this is necessary for Python. There’s very little you can usefully do with a half-destroyed object. That which you can do, you could also do without being exposed to half-destroyed objects. —Rhamphoryncus
-
They seem deliberately vague. Java distinguishes finalized from non-finalized objects, and a single finalizer is ordered with regard to non-finalized objects. The catch is that it’s not ordered with regard to other finalizers, so you need to program as if they may already be deleted. In practise this means avoiding finalizers unless absolutely necessary, and if necessary they must not depend on each other.
API compatibility in detail
It is barely credible that CPython might someday make tp_traverse mandatory for pointer-carrying types; adding support for write barriers or stack bookkeeping to the Python/C API seems extremely unlikely.
Another issue in this area is that existing C extensions depend on the GIL guarantees. They assume that when extension code is called, all other threads are locked out. If an extension does need to deal with a threaded environment, it explicitly opts in (by releasing the GIL). Therefore any would-be GIL replacement must provide GIL-like guarantees by default. Threading must remain opt-in for extensions.
Потоковые и многопроцессорные модули на Python
![]()
Главная идея потоков заключается в выполнении последовательности таких инструкций внутри программы, которые могут выполняться независимо от другого кода.
Так в чём же разница между потоковой и многопроцессорной обработкой данных? При одновременном выполнении нескольких задач обычно используется потоковая обработка, а при процессно-ориентированном параллелизме задействуется многопроцессорная обработка.
Задачи с ограничением скорости вычислений и ввода-вывода
Время выполнения задач, ограниченных скоростью вычислений, полностью зависит от производительности процессора, тогда как в задачах I/O Bound скорость выполнения процесса ограничена скоростью системы ввода-вывода.
В задачах с ограничением скорости вычислений программа расходует большую часть времени на использование центрального процессора, то есть на выполнение вычислений. К таким задачам можно отнести программы, занимающиеся исключительно перемалыванием чисел и проведением расчётов.
В задачах, ограниченных скоростью ввода-вывода, программы обрабатывают большие объёмы данных с диска в сравнении с необходимым объёмом вычислений. К таким задачам можно отнести, например, подсчёт количества строк в файле.
Проблема GIL на Python
Обычно на Python используется только один поток для выполнения нескольких записанных инструкций, то есть одновременно выполняется только один поток. Производительность однопоточного и многопоточного процессов здесь одинакова, и происходит это из-за GIL (Global Interpreter Lock — глобальной блокировки интерпретатора). Эта глобальная блокировка интерпретатора сама действует как поток и ограничивает другие потоки, делая невозможной многопоточность на Python.
Процессы ускоряют операции на Python, которые создают интенсивную вычислительную нагрузку на центральный процессор, используя сразу несколько ядер и избегая GIL, в то время как потоки лучше подходят для задач ввода-вывода или задач, связанных со внешними системами, потому что потоки могут более эффективно работать вместе. Для объединения процессов им нужно сериализовывать свои результаты, на что требуется время.
Потоки на Python не дают никаких преимуществ для задач, создающих интенсивную вычислительную нагрузку на процессор, именно из-за GIL.
Зачем нужен GIL?
Потоковый модуль использует потоки, многопроцессорный модуль использует процессы. Разница в том, что потоки выполняются в одном и том же пространстве памяти, а у процессов отдельная память. Это немного затрудняет совместное использование объектов процессами с многопроцессорной обработкой. В этом случае обычно выполняется сериализация объектов. Но потоки используют одну память, поэтому нужно быть осторожным, иначе два потока будут записывать данные в одну и ту же память одновременно. Именно для этого и существует глобальная блокировка интерпретатора.
Если бы мы запустили на Python скрипт, выполняющий простую задачу — спать (ну очень времязатратную!), он выглядел бы так: