Включите исполнение JavaScript в браузере, чтобы запустить приложение.
19 янв 2025

GIL: основные принципы в Python

GIL — своеобразная блокировка, которая позволяет только одному потоку управлять интерпретатором Python. Это означает, что в любой момент времени будет проводиться лишь одна операция. В статье рассмотрим, как GIL влияет на производительность программ и ограничивает многозадачность, а также разберемся, можно ли его отключать и зачем.

Что такое GIL в Python и зачем он нужен

GIL, или Global Interpreter Lock, защищает доступ к объектам Python, предотвращая одновременное выполнение байт-кодов несколькими потоками. Это работает даже в многопоточном приложении. Такая блокировка необходима, поскольку управление памятью в Python не считается потокобезопасным. 

Прежде чем углубиться в GIL, рассмотрим концепции, связанные с параллелизмом.

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

Многопоточное программирование — это метод, который позволяет выполнять несколько задач одновременно. Для этого одновременно используется вычислительная мощность нескольких процессоров, что приводит к повышению производительности. Но многопоточность не так проста, она требует тщательного управления общими ресурсами и синхронизации между потоками, чтобы избегать конфликтов. При правильной реализации многопоточность — хороший способ повысить эффективность и быстродействие кода.

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

В Python потоки реализованы как pthreads (стандарт EEE POSIX 1003.1c для Linux и macOS). Pthreads — это потоки на уровне операционной системы, то есть за управление и планирование отвечает ОС хоста. Многопоточная программа на Python никогда не будет по-настоящему параллельной: в Python благодаря блокировке GIL выполняется лишь один поток.

В потокобезопасной программе потоки могут свободно обращаться к одним и тем же структурам данных, ведь механизм синхронизации всегда поддерживает их в согласованном состоянии. Python использует для поддержки этой синхронизации GIL. Стратегия управления памятью, используемая в Python, нужна для защиты от некорректного создания объектов. 

Как GIL ограничивает многозадачность в Python

GIL работает путем наложения блокировки на каждую переменную и ведения счетчика использования. Если поток хочет получить доступ к переменной, которая уже используется другим потоком, он должен дождаться, пока тот освободит переменную.

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

import threading

def fib(n):

   if n <= 1:

       return n

   else:

       return fib(n-1) + fib(n-2)

def compute_fib():

   for i in range(30):

       print(fib(i))

thread_1 = threading.Thread(target=compute_fib)

thread_2 = threading.Thread(target=compute_fib)

thread_1.start()

thread_2.start()

thread_1.join()

thread_2.join()
py

В этом примере определяется функция fib, которая вычисляет n-е число Фибоначчи. Также определяется функция compute_fib для вычисления первых 30 чисел Фибоначчи. Создается два потока, которые вызывают compute_fib. Из-за GIL два потока не могут выполнять байт-коды Python параллельно. В результате время выполнения программы будет равняться времени выполнения обоих потоков.

Влияние GIL на производительность многопоточных программ

Теоретически многопоточность должна позволять программе работать быстрее за счет одновременного выполнения нескольких задач. Но это не относится к задачам, привязанным к процессору. 

Пример:

import threading

import time

def cpu_bound_task():

    count = 0

    for _ in range(10**7):

        count += 1

start = time.time()

threads = []

for _ in range(2):

    thread = threading.Thread(target=cpu_bound_task)

    thread.start()

    threads.append(thread)

for thread in threads:

    thread.join()

print(f"Execution time: {time.time() - start}")
py

В этом примере есть два потока, выполняющих задачу, привязанную к процессору. Время выполнения должно уменьшиться вдвое, но из-за GIL этого не происходит, потому что потоки выполняются последовательно.

Из-за GIL многопоточные программы, привязанные к процессору, могут выполняться медленнее, чем ожидалось.  Но не все программы на Python привязаны к процессору, и не все, соответственно, будут подвержены влиянию GIL. Для программ, связанных с вводом-выводом, потоки на Python могут быть эффективными. Это связано с тем, что пока один поток ожидает ввода-вывода, GIL может быть освобожден для других.

Как GIL влияет на выполнение CPU-bound и I/O-bound задач

I/O-bound задачи — это те, которые тратят большую часть времени на ожидание операций ввода-вывода, таких как чтение с диска или запись на него по сети. GIL оказывает менее выраженное влияние на программы с таким функционалом — ведь потоки освобождают GIL во время ожидания завершения этих операций. Благодаря этому можно запускать другие потоки и более эффективно использовать ресурсы — так растет производительность.

Пример, в котором несколько потоков считывают данные из файла:

import threading

def read_file(file_path):

    with open(file_path, 'r') as file:

        return file.read()

def worker(file_path):

    content = read_file(file_path)

    print(content)

threads = []

for i in range(4):

    t = threading.Thread(target=worker, args=('example.txt',))

    threads.append(t)

    t.start()

for t in threads:

    t.join()
py

Чтобы количественно оценить влияние GIL на CPU-bound задачи, можно сравнить время выполнения однопоточной и многопоточной реализации одной и той же задачи:

import threading

import time

# Function to compute sum of squares

def sum_of_squares(n):

    return sum(i * i for i in range(n))

# Single-threaded version

def single_threaded(n, num_threads):

    for _ in range(num_threads):

        sum_of_squares(n)

# Worker function for multi-threaded execution

def worker(n):

    sum_of_squares(n)

# Main execution starts here

start_time = time.time()

single_threaded(10**7, 4)

print(f"Single-threaded time: {time.time() - start_time:.2f} seconds")

start_time = time.time()

threads = []

for i in range(4):

    t = threading.Thread(target=worker, args=(10**7,))

    threads.append(t)

    t.start()

for t in threads:

    t.join()

print(f"Multi-threaded time: {time.time() - start_time:.2f} seconds")
py

Выполнение этого теста показывает, что многопоточная версия незначительно превосходит однопоточную по времени выполнения — из-за Global Interpreter Lock.

Обход ограничений GIL: использование C-расширений и альтернатив

GIL можно обойти и сделать программы на Python параллельными с помощью этих способов: 

  • многопроцессорность — вместо потоков можно использовать процессы. Модуль многопроцессорной обработки в Python создает отдельные процессы, каждый со своим собственным GIL. Это обеспечивает параллельное выполнение;
  • собственные расширения — части кода, привязанные к процессору, можно написать на таком языке, как C или Cython, а затем использовать собственные API-интерфейсы расширений Python для запуска этого кода. Он может выполняться вне GIL;
  • альтернативные интерпретаторы Python — есть вариант применять Jython, IronPython или PyPy. У них нет GIL. Но они бывают не на 100% совместимы с Python и могут работать не со всеми библиотеками, которые поддерживает этот язык.

Отключение GIL в Python 3.13

В Python 3.13 есть экспериментальная функция, позволяющая полностью отключить GIL. Это означает, что потоки могут выполняться более синхронно, что повышает производительность для определенных типов рабочих нагрузок.

Чтобы воспользоваться преимуществами этой функции, необходимо загрузить и установить бета-версию (rc1) Python 3.13 с конфигурацией многопоточной сборки. После этого легко включить или отключить GIL, используя переменные окружения или параметры командной строки.

Как другие языки справляются с многопоточностью

C и C++ предоставляют низкоуровневый контроль над многопоточностью с помощью библиотек POSIX threads (pthreads) в Unix-подобных системах и Windows threads API в Windows. Эти библиотеки обеспечивают широкий контроль, но требуют тщательного управления для предотвращения проблем параллелизма. Разработчикам приходится вручную обрабатывать синхронизацию и регулировать совместное использование ресурсов.

Java интегрирует многопоточность в свои основные языковые возможности с помощью java.lang.Thread и java.util.concurrent. Многопоточная модель Java построена поверх JVM (Java Virtual Machine), что позволяет уйти от многих сложностей работы с потоками. JVM управляет планированием и предоставляет высокоуровневые утилиты для обеспечения параллелизма. Это упрощает написание многопоточных приложений и управление ими. Такое подход помогает создавать масштабируемые приложения, особенно в серверных и корпоративных средах.

C# обеспечивает надежную поддержку многопоточности с помощью System.Threading и Task Parallel Library (TPL). Эти платформы предоставляют высокоуровневые абстракции для создания потоков и управления ими, а также инструменты для параллельного программирования. TPL упрощает разработку параллельного и асинхронного кода, написание адаптивных и масштабируемых приложений.

Будущее Python без GIL: перспективы и ожидания

Руководящий совет Python описал три этапа, которые, должен пройти язык без блокировки, чтобы стать основной версией для всех пользователей.

В краткосрочной перспективе. В версии Python 3.13 уже внедрили сборку без GIL в качестве экспериментального режима. Этот режим используется для получения информации о использовании версии, дизайне API, упаковке.

В среднесрочной перспективе. Как только будет уверенность в достаточной поддержке сообществом сборки Python без использования GIL, она станет поддерживаемой, хоть пока и не используемой по умолчанию. Задача — сделать ее со временем стандартной. Сроки будут зависеть от таких факторов, как совместимость API и готовность сообщества. По оценкам руководящего совета, этот этап может занять год или два.

В далекой перспективе. На этом этапе сборка Python без блокировки должна стать базовой. На это может потребоваться до 5 лет. На протяжении всего процесса будет регулярно оцениваться прогресс.