Привет, уважаемые читатели!
GIL, или Global Interpreter Lock десятилетиями оставался темой обсуждения и дебатов среди питонистов.
Что такое GIL? GIL, сокращение от Global Interpreter Lock, представляет собой важную концепцию в Python. Он представляет собой мьютекс, который блокирует доступ к объекту Python interpreter в многопоточных средах, разрешая выполнять лишь одну инструкцию за раз. Этот механизм, хоть и заботится о безопасности и целостности данных, одновременно становится камнем преткновения для тех, кто стремится максимально задействовать многозадачность и использовать полностью потенциал многоядерных процессоров.
Когда мы говорим о многозадачности в Python, имеется в виду использование множества потоков или процессов для выполнения различных задач. Это особенно актуально в приложениях, которые требуют обработки данных в реальном времени или одновременного выполнения большого числа задач. Однако GIL вносит ограничения в этот процесс, так как только один поток имеет доступ к интерпретатору Python в определенный момент времени.
В начальных версиях Python, GIL не существовал. Однако, когда Python начал использоваться для многопоточных приложений, стало очевидным, что возникают проблемы с одновременным доступом к общим ресурсам. Поэтому Гвидо ван Россум и команда разработчиков внедрили GIL, чтобы обеспечить безопасность работы с памятью и объектами Python.
GIL был введен не как намеренное ограничение, а скорее как необходимая мера для обеспечения безопасности в среде многозадачности.
Python создавался с упором на простоту и удобство разработки, и многие внутренние структуры данных Python, такие как списки и словари, могут быть изменены в процессе выполнения программы. Это делает Python удобным для использования, но также создает потенциальные проблемы в многопоточной среде. Без GIL, множество потоков могли бы одновременно изменять и взаимодействовать с этими структурами данных, что привело бы к непредсказуемому поведению и разнообразным гонкам данных.
Важным этапом было внедрение GIL в версии 1.5 Python. От этого момента GIL оставался фундаментальной частью ядра Python. Со временем, по мере развития языка, разработчики предпринимали попытки улучшить многозадачность и сделать GIL менее ограничивающим.
В версии Python 3.2 была внедрена система, позволяющая разделить блокировки GIL на несколько частей, что дало небольшой прирост производительности в определенных случаях.
Как работает GIL
GIL - это мьютекс, который действует как ограничитель, позволяющий только одному потоку выполнять байткод Python в один момент времени. Это означает, что в многозадачной среде Python, в один и тот же момент времени только один поток может активно выполнять Python-код.
Пример:
import threading
def worker():
for _ in range(1000000):
pass
# Создаем два потока
thread1 = threading.Thread(target=worker)
thread2 = threading.Thread(target=worker)
# Запускаем потоки
thread1.start()
thread2.start()
# Ждем, пока оба потока завершатся
thread1.join()
thread2.join()
В приведенном примере два потока выполняют функцию worker
, которая просто выполняет цикл. Однако из-за GIL только один из потоков будет активен в определенный момент времени. Это ограничение может существенно влиять на производительность, особенно в многозадачных приложениях.
Python предоставляет встроенный модуль threading
для работы с потоками. Важно отметить, что GIL существует на уровне интерпретатора Python и не зависит от операционной системы. Поэтому, даже если ваша операционная система поддерживает многозадачность, GIL может ограничивать использование нескольких ядер процессора.
Чтобы работать с потоками в Python, вы можете создавать экземпляры класса Thread
из модуля threading
и запускать их. Важно помнить, что GIL ограничивает многозадачность на уровне интерпретатора, поэтому потоки в Python подходят для задач, которые больше связаны с ожиданием ввода-вывода, чем с интенсивной обработкой данных.
Пример:
import threading
def print_numbers():
for i in range(1, 6):
print(f"Number: {i}")
def print_letters():
for letter in 'abcde':
print(f"Letter: {letter}")
# Создаем два потока
thread1 = threading.Thread(target=print_numbers)
thread2 = threading.Thread(target=print_letters)
# Запускаем потоки
thread1.start()
thread2.start()
# Ждем, пока оба потока завершатся
thread1.join()
thread2.join()
В этом примере мы создаем два потока для вывода чисел и букв. Обратите внимание, что блокировка GIL не влияет на этот пример, так как он включает ожидание вывода на экране, что является операцией ввода-вывода.
Взаимодействие потоков с GIL может привести к неожиданным результатам, особенно если не учитывать блокировки и многозадачность. Когда несколько потоков пытаются изменить одни и те же данные, могут возникнуть гонки данных (race conditions).
Пример:
import threading
counter = 0
def increment():
global counter
for _ in range(1000000):
counter += 1
# Создаем два потока
thread1 = threading.Thread(target=increment)
thread2 = threading.Thread(target=increment)
# Запускаем потоки
thread1.start()
thread2.start()
# Ждем, пока оба потока завершатся
thread1.join()
thread2.join()
print("Counter:", counter)
В этом примере два потока пытаются инкрементировать общий счетчик. Вследствие блокировки GIL, результат этой операции может быть неопределенным и зависит от того, какой поток получит доступ к счетчику в данный момент.
Проблемы, связанные с GIL
IV. Проблемы, связанные с GIL
Сейчас, когда мы более глубоко понимаем, как GIL работает, пришло время рассмотреть ряд проблем, связанных с его присутствием и влиянием на многозадачность и производительность в Python. В данном разделе мы представим вам десять типичных проблем, с которыми сталкиваются профессиональные разработчики, а также предоставим примеры кода, иллюстрирующие каждую из них.
-
Ограниченная многозадачность: Одна из наиболее известных проблем GIL - это ограничение на многозадачность. Не смотря на наличие множества потоков, лишь один может активно выполняться в определенный момент времени.
Пример:
import threading def count_up(): for i in range(1000000): pass thread1 = threading.Thread(target=count_up) thread2 = threading.Thread(target=count_up) thread1.start() thread2.start() thread1.join() thread2.join()
-
Производительность многозадачных приложений: Многозадачные приложения, которые должны эффективно использовать многие ядра процессоров, могут столкнуться с проблемами производительности, так как GIL ограничивает параллельное выполнение.
Пример:
import threading def compute_square(num): return num * num def main(): numbers = list(range(1000)) results = [] for number in numbers: thread = threading.Thread(target=lambda num=number: results.append(compute_square(num))) thread.start() for thread in threading.enumerate(): if thread != threading.current_thread(): thread.join() if __name__ == "__main__": main()
-
Проблемы с вводом-выводом: GIL не так сильно ограничивает операции ввода-вывода, поэтому приложения, ориентированные на ожидание данных из файлов, сети и других источников, могут работать относительно нормально.
Пример:
import threading import requests def download_url(url): response = requests.get(url) content_length = len(response.text) print(f"Downloaded {url} with {content_length} characters.") urls = ["https://example.com", "https://example.org", "https://example.net"] threads = [] for url in urls: thread = threading.Thread(target=download_url, args=(url,)) thread.start() threads.append(thread) for thread in threads: thread.join()
-
Сложности с разделением данных: Поделить данные между потоками может быть сложной задачей из-за GIL. Это может привести к гонкам данных и ошибкам.
Пример:
import threading shared_data = [] lock = threading.Lock() def append_data(data): with lock: shared_data.append(data) thread1 = threading.Thread(target=append_data, args=("Hello",)) thread2 = threading.Thread(target=append_data, args=("World",)) thread1.start() thread2.start() thread1.join() thread2.join() print(shared_data)
-
Нестабильное время выполнения: Из-за конкуренции за GIL, время выполнения кода в потоках может быть непредсказуемым и меняться от запуска к запуску.
Пример:
import threading def count_up(): total = 0 for i in range(1000000): total += i print(f"Total: {total}") thread1 = threading.Thread(target=count_up) thread2 = threading.Thread(target=count_up) thread1.start() thread2.start() thread1.join() thread2.join()
-
Ограничения на ресурсы: GIL также ограничивает доступ к ресурсам компьютера, таким как процессорное время, что может быть проблематично для многозадачных приложений.
Пример:
import threading import time def heavy_calculation(): result = 0 for _ in range(100000000): result += 1 time.sleep(5) thread1 = threading.Thread(target=heavy_calculation) thread2 = threading.Thread(target=heavy_calculation) thread1.start() thread2.start() thread1.join() thread2.join()
-
Особенности на многопроцессорных системах: На многопроцессорных системах GIL может привести к неэффективному использованию ресурсов, так как несколько ядер могут быть неактивными.
Пример:
import threading def cpu_bound_task(): total = 0 for _ in range(100000000): total += 1 print(f"Total: {total}") thread1 = threading.Thread(target=cpu_bound_task) thread2 = threading.Thread(target=cpu_bound_task) thread1.start() thread2.start() thread1.join() thread2.join()
-
Неэффективное использование многоядерных процессоров: GIL делает Python менее эффективным на многоядерных процессорах, так как только одно ядро может быть активным в данный момент.
Пример:
import threading def compute_squares(numbers): return [x * x for x in numbers] numbers = list(range(1000000)) thread1 = threading.Thread(target=compute_squares, args=(numbers,)) thread2= threading.Thread(target=compute_squares, args=(numbers,)) thread1.start() thread2.start() thread1.join() thread2.join()
Сложности с реализацией реальной многозадачности: Из-за GIL, реализация настоящей многозадачности в Python может быть более сложной и требовательной к ресурсам.
Пример:
import threading def perform_task(task_name): print(f"Performing task: {task_name}") tasks = ["Task 1", "Task 2", "Task 3"] threads = [threading.Thread(target=perform_task, args=(task,)) for task in tasks] for thread in threads: thread.start() for thread in threads: thread.join()
Сложности с параллельной обработкой данных: Параллельная обработка данных может быть затруднительной из-за GIL, особенно при работе с большими объемами данных.
Пример:
import threading def process_data(data): result = [] for item in data: result.append(item * 2) return result data = list(range(1000000)) thread1 = threading.Thread(target=process_data, args=(data,)) thread2 = threading.Thread(target=process_data, args=(data,)) thread1.start() thread2.start() thread1.join() thread2.join()
Важно понимать, что GIL - это не ошибка, а концепция, заложенная в Python с целью обеспечения безопасности и упрощения управления памятью. Однако, он также создает ряд ограничений для многозадачных приложений, и разработчики должны учитывать его при проектировании и оптимизации кода.
Способы обхода GIL
Один из наиболее эффективных способов обойти GIL - это использование многопроцессорной обработки вместо многозадачных потоков. Поскольку каждый процесс имеет свой собственный интерпретатор Python и собственный GIL, они могут параллельно выполняться на разных ядрах процессора.
Пример использования многопроцессинга в Python с использованием модуля multiprocessing
:
import multiprocessing
def worker(data):
# Здесь происходит обработка данных
result = data * 2
return result
data = [1, 2, 3, 4, 5]
# Создаем пул процессов
pool = multiprocessing.Pool(processes=multiprocessing.cpu_count())
# Используем многопроцессорный пул для обработки данных
results = pool.map(worker, data)
# Завершаем пул
pool.close()
pool.join()
print("Результаты:", results)
Этот код создает пул процессов и использует его для параллельной обработки данных. Это позволяет эффективно использовать многозадачность и обойти ограничения GIL.
Помимо multiprocessing
, существует несколько библиотек и фреймворков, которые предоставляют более высокоуровневый доступ к многопроцессорной обработке. Например, concurrent.futures
позволяет использовать пулы потоков и процессов, предоставляя удобный интерфейс для выполнения параллельных задач.
Пример использования concurrent.futures
с пулом потоков:
import concurrent.futures
def worker(data):
# Здесь происходит обработка данных
result = data * 2
return result
data = [1, 2, 3, 4, 5]
# Создаем пул потоков
with concurrent.futures.ThreadPoolExecutor() as executor:
results = list(executor.map(worker, data))
print("Результаты:", results)
Используя concurrent.futures
, вы можете легко переключаться между пулами потоков и процессов в зависимости от требований вашего приложения.
Еще одним способом обойти GIL является использование C-расширений. Python позволяет создавать расширения на C, которые могут выполнять интенсивные операции без блокировки GIL. Эти расширения могут взаимодействовать напрямую с системными вызовами операционной системы и использовать все преимущества многозадачности.
Пример создания C-расширения для Python:
#include <Python.h>
static PyObject* my_extension_function(PyObject* self, PyObject* args) {
// Здесь можно выполнять интенсивные вычисления
int result = 0;
// ...
return Py_BuildValue("i", result);
}
static PyMethodDef my_extension_methods[] = {
{"my_extension_function", my_extension_function, METH_VARARGS, "Описание функции"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef my_extension_module = {
PyModuleDef_HEAD_INIT,
"my_extension",
"Описание модуля",
-1,
my_extension_methods
};
PyMODINIT_FUNC PyInit_my_extension(void) {
return PyModule_Create(&my_extension_module);
}
Затем этот C-расширение можно использовать в Python, обеспечивая более эффективное выполнение интенсивных операций.
Советы по оптимизации производительности
Если ваши потоки часто блокируются, например, из-за операций ввода-вывода, это может значительно ухудшить производительность. Вместо блокировки потока, можно использовать неблокирующие операции ввода-вывода или асинхронный код, чтобы избежать простоя потоков.
Пример использования неблокирующих операций ввода-вывода:
import socket
def non_blocking_network_operation():
# Создание неблокирующего сокета
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.setblocking(0)
try:
# Попытка подключения без блокировки
sock.connect(("example.com", 80))
except BlockingIOError:
pass
# Продолжение выполнения кода без блокировки
Для оптимизации производительности можно разбить код на независимые задачи и выполнять их параллельно. Вместо использования потоков Python, которые могут столкнуться с GIL, рассмотрите использование более низкоуровневых механизмов, таких как процессы или асинхронное программирование.
Пример использования асинхронного кода с библиотекой asyncio
:
import asyncio
async def async_task():
await asyncio.sleep(1)
print("Выполнение асинхронной задачи")
async def main():
tasks = [async_task() for _ in range(10)]
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
Асинхронное программирование позволяет эффективно управлять задачами без блокировки потоков.
Советы по оптимизации производительности
-
Используйте встроенные функции и методы:
В Python существует множество встроенных функций и методов, которые оптимизированы и быстрее, чем ручные аналоги. Например, вместо обхода списка цикломfor
, используйте функцииmap()
,filter()
,sum()
и другие.numbers = [1, 2, 3, 4, 5] # Плохой способ total = 0 for num in numbers: total += num # Хороший способ total = sum(numbers)
-
Используйте генераторы:
Генераторы в Python позволяют лениво генерировать значения и могут сэкономить память и увеличить производительность.# Плохой способ squares = [] for num in range(1, 1000000): squares.append(num ** 2) # Хороший способ squares = (num ** 2 for num in range(1, 1000000))
-
Избегайте избыточных вычислений:
Если вы выполняете одни и те же вычисления несколько раз, сохраните результат и используйте его повторно.# Плохой способ result1 = complex_computation(data) result2 = complex_computation(data) # Хороший способ result = complex_computation(data) result1 = result result2 = result
-
Используйте set вместо списка для быстрого поиска:
Если вам часто приходится искать элементы в коллекции, используйте множества (set), которые имеют гораздо более быстрое время доступа, чем списки.# Плохой способ items = [1, 2, 3, 4, 5] if 3 in items: print("Найден!") # Хороший способ items = {1, 2, 3, 4, 5} if 3 in items: print("Найден!")
-
Оптимизируйте работу с файлами:
При работе с файлами используйте контекстные менеджеры для автоматического закрытия файлов. Кроме того, читайте и записывайте данные порциями, чтобы уменьшить использование памяти.# Плохой способ file = open("data.txt", "r") data = file.read() file.close() # Хороший способ with open("data.txt", "r") as file: data = file.read(1024)
-
Используйте функции из стандартной библиотеки:
Python имеет множество функций и модулей в стандартной библиотеке для обработки данных, парсинга XML, работы с JSON и других задач. Вместо написания собственных решений, используйте уже существующие.# Плохой способ import my_custom_parser data = my_custom_parser.parse_xml(xml_data) # Хороший способ import xml.etree.ElementTree as ET root = ET.fromstring(xml_data)
-
Избегайте многократных операций I/O:
Операции ввода-вывода, такие как чтение и запись файлов или сетевые запросы, могут быть затратными. При выполнении множества таких операций объединяйте их и выполняйте одним запросом.# Плохой способ for url in urls: response = requests.get(url) process_data(response.text) # Хороший способ responses = [requests.get(url) for url in urls] for response in responses: process_data(response.text)
-
Используйте алгоритмы с линейным временем выполнения:
При выборе алгоритмов старайтесь использовать те, которые имеют линейное время выполнения (O(n)), чтобы избежать долгих операций.# Плохой способ def find_max(numbers): max_num = numbers[0] for num in numbers: if num > max_num: max_num = num return max_num # Хороший способ max_num = max(numbers)
-
Используйте профилирование:
Профилирование вашего кода помогает выявить места, где тратится больше всего времени, и сосредоточить усилия на оптимизации важных частей.Пример использования модуля
cProfile
:import cProfile def my_function(): # Код для профилирования cProfile.run("my_function()")
-
Избегайте использования глобальных переменных:
Глобальные переменные могут сделать код менее читаемым и управляемым. Вместо них используйте передачу параметров в функции и возвращение результатов.# Плохой способ count = 0 def increment_count(): global count count += 1 # Хороший способ def increment_count(count): return count + 1 count = increment_count(count)
Заключение
GIL - это особенность интерпретатора Python, которая ограничивает одновременное выполнение нескольких потоков Python-кода в одном процессе. Это ограничение может стать вызовом для разработчиков, особенно тех, кто сталкивается с многозадачностью и параллельной обработкой данных.
Больше практических навыков вы можете получить у экспертов онлайн-курса Python Developer. Professional. Также хочу порекомендовать вебинары про асинхронное взаимодействие в Python и Tracing в приложениях на Python, на которые вы можете зарегистрироваться абсолютно бесплатно.
Комментарии (15)
Revertis
24.10.2023 11:03+2Однако, когда Python начал использоваться для многопоточных приложений, стало очевидным, что возникают проблемы с одновременным доступом к общим ресурсам. Поэтому Гвидо ван Россум и команда разработчиков внедрили GIL, чтобы обеспечить безопасность работы с памятью и объектами Python.
Взаимодействие потоков с GIL может привести к неожиданным результатам, особенно если не учитывать блокировки и многозадачность. Когда несколько потоков пытаются изменить одни и те же данные, могут возникнуть гонки данных (race conditions).
Звучит так, как будто хотели пофиксить проблему, но в итоге не пофиксили, а сделали хуже.
По-моему, надо было сделать два режима работы программы. Один основной, с GIL, а второй без него - при запуске программы она бы делала что-то вроде
GIL.disable()
, и использовала бы потом мютексы, рвлоки и т.п.funca
24.10.2023 11:03По-моему, надо было сделать два режима работы программы
Вот тут уже делают по-вашему https://github.com/colesbury/nogil :) Кстати, у них классно описаны компромиссы для GIL и non-GIL реализаций в документе Python Multithreading without GIL.
В целом, GIL даёт гарантии, что состояние самого интерпретатора не будет поломано в многопоточной среде. Это на самом деле очень крутая фича, без которой отладка пользовательского кода при невнимательном использовании потоков превратилась бы в ад.
Сейчас в PEP 684 (python 3.12) и PEP 554 (хотят в 3.13, но скорее позже) проблему пытаются фиксить ещё и с другой стороны - избавляясь от излишне глобального и мутабельного состояния интерпретатора везде, где только можно. Но там ещё куча работы.
Revertis
24.10.2023 11:03+1Сейчас в PEP 684 (python 3.12) и PEP 554 (хотят в 3.13, но скорее позже) проблему пытаются фиксить ещё и с другой стороны - избавляясь от излишне глобального и мутабельного состояния интерпретатора везде, где только можно.
Есть опасения, что это скорее усложнит код самого интерпретатора, но цель останется недостигнута.
funca
24.10.2023 11:03+1На практике есть куча задач, которые прекрасно решаются в один поток как в node.js, без всей этой чехорды.
Revertis
24.10.2023 11:03+2И есть куча задач, которые хорошо распараллеливаются, и получают огромный буст от запуска на всех ядрах процессора.
funca
24.10.2023 11:03Для этого есть масса способов https://www.nersc.gov/assets/Uploads/07-Scaling-Python-Applications.pdf. Например, чтобы распараллеливать вычисления вам не нужна многопоточная программа - вам нужно много однопоточных программ, исполняющихся в разных потоках. Эта модель покрывается sub-interpreters из PEP 684 (проблема лишь в бинарных библиотеках, которые могут пока не быть рассчитаны на такое использование). Многопоточность, когда именно самой программе нужно прерываться это скорее десктопная история ради отзывчивости. Но и здесь можно оставить ui в один поток, а вычисления вынести в воркеры.
slonopotamus
24.10.2023 11:03+2Например, чтобы распараллеливать вычисления вам не нужна многопоточная программа - вам нужно много однопоточных программ, исполняющихся в разных потоках.
Много однопоточных программ менее эффективны чем одна многопоточная.
avkritsky
24.10.2023 11:03Не понял в чём разница плохого варианта от хорошего в 7 совете. Созданием двух циклов?
koldyr
24.10.2023 11:03+2"Используйте алгоритмы с линейным временем выполнения:
При
выборе алгоритмов старайтесь использовать те, которые имеют линейное
время выполнения (O(n)), чтобы избежать долгих операций."Просто совет дня.
Можно ещё добавить старайтесь не использовать квадратичные и ни в коем случае не используйте экспоненциальные. И будет прямо как три закона роботехники.
titan_pc
24.10.2023 11:03+6Ну уберут из питона GIL, дадут всем дженерики, через пару лет за счёт этого создадут первый официальный компилятор питона в бинарник. И что получиться, голанг какой-нибудь...
Китайские мультитулы это конечно весело. Но когда нужно копьё, как запихать в карман копьё? Какого зверя идём бить - такой и инструмент берём.
Интересно кто-то думал, когда шёл в it, что вот сейчас курс по питону от Яндекса пройду вот и буду горы денег зашибать.
Упёрлись в gil - значит доросли до копья. И пора осваивать копьё.
С языками ещё и инфастуктурщина всякая липнет. Докеры, куберы, гитлабы, дженкинс. Чат жпт вот появился. Улучшайзеры, генераторы, идеешки, плагины, базы данных, ресурсы, мощности, хостинг...
Но почему то вот язык у большинства людей в голове он как то оторван вот от остального... Мол на одном языке продукт создам на 1000 000 пользователей... Ща вот освою питон и как жахну тикток+.
Изучил питон, освоил Джил. Иди го ковыряй. Упёрся в GC - иди rust ковыряй. Там упёрся в сложно понять куда - иди в ассемблер.
Чем раньше это будет понято, тем больше миру пользы. И обсуждений зачем создали Джил и для кого - станет меньше. Особенно советов питонистам, как его обходить. На первой строке такого совета должно быть "Открываем книжку по голангу".
Dominux
24.10.2023 11:03Автор говорит о том, что GIL ограничивает выполнение CPU-bound задач, при этом демонстрируя пример с использованием модуля threading, который работает именно с потоками, которые никак не ускоряют их выполнение
arheops
24.10.2023 11:03+1Ну думаю, зайду, наконец-то почитаю КАК же все таки устоен GIL
Ответ - слишком сложно, это просто мьютекс... ага, ясно, понятно.
VadimChin
>> Вследствие блокировки GIL, те без оного все будет ок?