Выбрать главу

Джефф Напп (Jeff Knupp), автор книги Writing Idiomatic Python (http://bit.ly/writing-idiomatic-python), написал статью о том, как обойти GIL (http://bit.ly/pythons-hardest-problems), процитировав статью Дэвида Бизли (David Beazley)[100] на эту тему.

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

Многопоточность

Библиотека для работы с потоками в Python позволяет создать несколько потоков. Из-за GIL (во всяком случае в CPython) только один процесс Python может быть запущен для каждого интерпретатора; это означает, что прирост производительности появится только в том случае, если хотя бы один поток заблокирован (например, для ввода/вывода). Еще один вариант для ввода/вывода — обработка событий. Чтобы узнать подробнее, прочтите абзацы, связанные с asyncio, в подразделе «Производительность сетевых инструментов из стандартной библиотеки Python» раздела «Распределенные системы» главы 9.

Когда у вас есть несколько потоков Python, ядро замечает, что один из потоков заблокирован для ввода-вывода, и оно переключается, чтобы позволить следующему потоку использовать процессор до тех пор, пока он не будет заблокирован или не завершится. Все это происходит автоматически, когда вы запускаете ваши потоки. Есть хороший пример применения многопоточности на сайте Stack Overflow (http://bit.ly/threading-in-python), для серии Python Module of the Week написана отличная статья на тему многопоточности (https://pymotw.com/2/threading/). Вы также можете просмотреть документацию стандартной библиотеки, посвященную работе с потоками (https://docs.python.org/3/library/threading.html).

Модуль multiprocessing

Модуль multiprocessing (https://docs.python.org/3/library/multiprocessing.html) стандартной библиотеки Python предоставляет способ обойти GIL путем запуска дополнительных интерпретаторов Python. Отдельные процессы могут общаться друг с другом с помощью запросов multiprocessing.Pipe или multiprocessing.Queue; также они могут делиться памятью с помощью запросов multiprocessing.Array или multiprocessing.Value, что автоматически реализует блокировки. Осторожно делитесь данными; эти объекты реализуют блокировку для того, чтобы предотвратить одновременный доступ разных процессов.

Рассмотрим пример, иллюстрирующий прирост скорости, который появляется в результате использования пула работников. Существует компромисс между сэкономленным временем и временем, затрачиваемым на переключение между интерпретаторами. В примере используется метод Монте-Карло для оценки значения числа пи[101]:

>>> import multiprocessing

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

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

Метод pool.map() создает несколько процессов — по одному на каждый элемент списка итераций; максимальное количество равно числу, указанному при инициализации пула (в вызове multiprocessing.Pool(processes)).

Существует только один процесс для первого испытания timeit.

вернуться

100

Дэвид Бизли написал отличное руководство (http://www.dabeaz.com/python/UnderstandingGIL.pdf), которое описывает способ работы GIL. Он также рассматривает новую версию GIL (http://www.dabeaz.com/python/NewGIL.pdf), появившуюся в Python 3.2. Результаты показывают, что максимизация производительности в приложении Python требует глубокого понимания GIL, его влияния на приложение, а также знания количества ядер и понимания узких мест приложения.

вернуться

101

По адресу http://bit.ly/monte-carlo-pi приводится полная реализация метода. По сути, вы бросаете дротики в квадрат размером 2 × 2, а круг имеет радиус = 1. Если дротик может попасть в любое место доски с одинаковой вероятностью, то процент дротиков, попавших в круг, будет равен π / 4. Это означает, что если вы умножите шанс попадания в круг на 4, то получите число π.