Способы обхода GIL для повышения производительности

Kate

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

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

В этой статье рассмотрим способы обхода GIL и первый способ - использование многопроцессности вместо многопоточности.

Использование многопроцессности вместо многопоточности​

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

Реализовать многопроцессность можно с помощью либы multiprocessing. Она дает возможность каждому процессу работать с собственным интерпретатором Python и, соответственно, собственным GIL. Т.е каждый процесс может полноценно использовать отдельное ядро процессора, обходя ограничения, налагаемые GIL на многопоточное выполнение кода.

Основные функции multiprocessing​

Блокировки используются для синхронизации доступа к ресурсам между разными процессами. Например, можно использовать Lock для гарантии того, что только один процесс может выполнять определенный участок кода одновременно:

from multiprocessing import Process, Lock

def printer(item, lock):
lock.acquire()
try:
print(item)
finally:
lock.release()

if __name__ == '__main__':
lock = Lock()
items = ['тест1', 'тест2', 'тест3']
for item in items:
p = Process(target=printer, args=(item, lock))
p.start()
Семафоры похожи на блокировки, но позволяют ограничить доступ к ресурсу не одним, а несколькими процессами одновременно:

from multiprocessing import Semaphore, Process

def worker(semaphore):
with semaphore:
# работа, требующая синхронизации
print('Работает')

if __name__ == '__main__':
semaphore = Semaphore(2)
for _ in range(4):
p = Process(target=worker, args=(semaphore,))
p.start()
События позволяют процессам ожидать сигнал от других процессов для начала выполнения определенных действий:

from multiprocessing import Process, Event
import time

def waiter(event):
print('Ожидание события')
event.wait()
print('Событие произошло')

if __name__ == '__main__':
event = Event()

for _ in range(3):
p = Process(target=waiter, args=(event,))
p.start()

print('Главный процесс спит')
time.sleep(3)
event.set()
Очереди в multiprocessing позволяют безопасно обмениваться данными между процессами:

from multiprocessing import Process, Queue

def worker(queue):
queue.put('Элемент от процесса')

if __name__ == '__main__':
queue = Queue()
p = Process(target=worker, args=(queue,))
p.start()
p.join()
print(queue.get())

Асинхронное программирование​

asyncio — это библиотека для написания конкурентного кода с использованием синтаксиса async/await, введенного в Python 3.5. Она служит базой для многих асинхронных фреймворков Python

В отличие от многопоточного исполнения, asyncio использует единственный поток и event loop для управления асинхронными операциями, что позволяет обходить ограничения, связанные с GIL.

Предположим, нужно собрать заголовки с нескольких веб-страниц. Будем юзатьaiohttp в качестве асинхронного HTTP-клиента для отправки запросов:

import asyncio
import aiohttp

async def fetch_title(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
html = await response.text()
return html.split('<title>')[1].split('</title>')[0]

async def main(urls):
tasks = [fetch_title(url) for url in urls]
titles = await asyncio.gather(*tasks)
for title in titles:
print(title)

urls = [
'https://example.com',
'https://example.org',
'https://example.net',
# предположим, здесь список из тысячи URL
]

asyncio.run(main(urls))
Функция fetch_title асинхронно извлекает HTML-контент для заданного URL и возвращает содержимое тега <title>. А main создает задачи для каждого URL и запускает их параллельно с помощью asyncio.gather(). Таким образом можно тысячи веб-запросов одновременно, оптимизируя время ожидания ответов от серверов и эффективно используя ресурсы.

Интеграция с внешними С/С++ модулями​

Весьма годный способ обхода ограничений GIL и повышения производительности при работе с CPU-интенсивными задачами. Создание расширений позволяет напрямую обращаться к системным вызовам и C-библиотекам, минуя оверхед интерпретатора Python и GIL.

Расширение Python на C или C++ представляет собой разделяемую библиотеку, которая экспортирует функцию инициализации. Функция возвращает полностью инициализированный модуль или экземпляр PyModuleDef. Для модулей с именами в ASCII необходимо, чтобы функция инициализации называлась PyInit_<имямодуля>. Для не ASCII имен модулей используется кодировка punycode и префикс PyInitU_.

Для создания модуля на C, начинаем с определения методов модуля и таблицы методов, а затем определяем сам модуль:

static PyObject *method_fputs(PyObject *self, PyObject *args) {
char *str, *filename = NULL;
int bytes_copied = -1;

if (!PyArg_ParseTuple(args, "ss", &str, &filename)) {
return NULL;
}

FILE *fp = fopen(filename, "w");
bytes_copied = fputs(str, fp);
fclose(fp);

return PyLong_FromLong(bytes_copied);
}

static PyMethodDef CustomMethods[] = {
{"fputs", method_fputs, METH_VARARGS, "Python interface to fputs C library function"},
{NULL, NULL, 0, NULL}
};

static struct PyModuleDef custommodule = {
PyModuleDef_HEAD_INIT,
"custom",
"Python interface for the custom C library function",
-1,
CustomMethods
};

PyMODINIT_FUNC PyInit_custom(void) {
return PyModule_Create(&custommodule);
}
После определения функции инициализации и методов модуля создаем setup.py файл, чтобы скомпилировать модуль:

from distutils.core import setup, Extension

setup(name="custom",
version="1.0",
description="Python interface for the custom C library function",
ext_modules=[Extension("custom", ["custommodule.c"])])
Выполнив команду python3 setup.py install в терминале модуль скомпилируется и установится став доступным для импорта в Python.

Python и C/C++ имеют разные системы исключений. Если к примеру нужно выбросить исключение Python из C-расширения можно использовать API Python для работы с исключениями. Например, чтобы выбросить ValueError если строка меньше 10 символов:

if (strlen(str) < 10) {
PyErr_SetString(PyExc_ValueError, "String length must be greater than 10");
return NULL;
}
Таким образом можно использовать предопределенные исключения Python или создать свои собственные.


Также некоторые библиотеки, к примеру как NumPy, Numba и Cython имеют встроенные возможности для обхода GIL.

 
Сверху