Параллелизм Python
Почему Taskgroup и тайм-аут так важны в Python 3.11 asyncio
Использование структурированного параллелизма в Python 3.11
Это мой первый пост в моей колонке Python Concurrency, и если вы сочтете его полезным, вы можете прочитать остальные здесь.
Новые функции пакета asyncio Python 3.11
1. Введение
В прошлогоднем выпуске Python 3.11 в пакет asyncio были добавлены TaskGroup
и timeout
API. Эти два API представили официальную функцию Structured Concurrency, которая помогает нам лучше управлять жизненным циклом параллельных задач. Сегодня я познакомлю вас с использованием этих двух API и значительными улучшениями, которые Python внес в наше параллельное программирование с появлением структурированного параллелизма.
2. Группа задач
TaskGroup
создается с помощью асинхронного менеджера контекста, и параллельные задачи могут быть добавлены в группу методом create_task
со следующим примером кода:
Когда диспетчер контекста завершает работу, он ожидает завершения всех задач в группе. Пока ждем, мы еще можем добавить новые задачи в TaskGroup
.
Обратите внимание: если задача в группе выдает исключение, отличное от asyncio.CancelledError
, во время ожидания, все остальные задачи в группе будут отменены.
Кроме того, все исключения, кроме asyncio.CanceledError
, будут объединены и выброшены в ExceptionGroup
.
3. время ожидания
asyncio.timeout
также создается с помощью асинхронного менеджера контекста. Он ограничивает время выполнения параллельного кода в контексте.
Предположим, что если нам нужно установить тайм-аут на один вызов функции, достаточно вызвать asyncio.wait_for
:
Но когда необходимо установить единый тайм-аут для нескольких одновременных вызовов, все становится проблематично. Предположим, у нас есть две параллельные задачи, и мы хотим, чтобы они выполнялись за 8 секунд. Давайте попробуем назначить каждой задаче среднее время ожидания 4 секунды с помощью кода, подобного следующему:
Вы можете видеть, что, хотя мы установили среднее время ожидания для каждого параллельного метода, такая настройка может привести к неконтролируемым ситуациям, поскольку не гарантируется одновременный возврат каждого вызова задачи, связанной с вводом-выводом, и мы все равно получили TimeoutError
.
На этом этапе мы используем блок asyncio.timeout
, чтобы убедиться, что мы установили общий тайм-аут для всех одновременных задач:
4. Что такое структурированный параллелизм
TaskGroup
и asyncio.timeout
выше используется функция async with
. Так же, как блок структуры with
может единообразно управлять жизненным циклом ресурсов следующим образом:
Но вызов параллельных задач внутри блока with
не работает, потому что параллельная задача продолжит выполнение в фоновом режиме, пока блок with
уже вышел, что приведет к некорректному закрытию ресурса:
Поэтому мы ввели здесь функцию async with
. Как и с, асинхронный си TaskGroup
используется для единообразного управления жизненным циклом параллельного кода, что делает код понятным и экономит время разработки. Сегодня мы называем эту функцию нашим главным героем: Structured Concurrency.
Почему структурированный параллелизм так важен
1. История параллельного программирования
До появления параллельного программирования мы выполняли наш код последовательно. Код будет выполнять циклы for_loop, условные переходы if_else и вызовы функций последовательно, в зависимости от порядка в стеке вызовов.
Однако по мере того, как скорость выполнения кода становилась все более требовательной с точки зрения вычислительной эффективности, а компьютерное оборудование значительно развивалось, постепенно возникло параллельное программирование (с привязкой к ЦП) и параллельное программирование (с привязкой к вводу-выводу).
До появления сопрограмм программисты Python использовали многопоточность для реализации параллельного программирования. Но у потоков Python есть проблема, то есть GIL (Global Interpreter Lock), существование GIL делает параллелизм на основе потоков неспособным достичь желаемой производительности.
Так появилась asyncio coroutine. Без GIL и переключения между потоками параллельное выполнение намного эффективнее. Если потоки — это переключение задач на основе временных интервалов, управляемое ЦП, то сопрограмма — это создание и переключение подзадач обратно в руки самого программиста. Хотя программисты наслаждаются удобством, они также сталкиваются с новым набором проблем.
2. Проблемы с моделью параллельного программирования
Как подробно описано в этой статье, параллельное программирование поднимает несколько вопросов, касающихся потока управления.
Параллельное программирование открывает несколько ветвящихся процессов в нашем основном потоке. Эти задачи ветвления молча выполняют сетевые запросы, доступ к файлам, запросы к базе данных и другие задачи в фоновом режиме.
Параллельное программирование изменит поток нашего кода с этого на это:
В соответствии с правилом программирования «низкая связанность, высокая связность» мы все хотим объединить все фоновые задачи в модуле после выполнения следующим образом:
Но дело в том, что, поскольку несколько участников разрабатывают наше приложение или вызывают множество сторонних компонентов, нам нужно знать, какие задачи все еще выполняются в фоновом режиме, а какие задачи завершены. Более вероятно, что одна фоновая задача разветвится на несколько других задач-ветвей.
В конечном счете, эти задачи ветвления должны быть найдены вызывающей стороной и дождаться завершения их выполнения, поэтому это выглядит так:
Хоть это и не мультивселенная Марвела, ситуация сейчас как раз похожа на мультивселенную, привносящую в наш естественный мир абсолютный хаос.
Некоторые читатели говорят, что asyncio.gather
может отвечать за объединение всех фоновых задач. Но у asyncio.gather
есть свои проблемы:
- Он не может централизованно управлять внутренними задачами унифицированным способом. Часто создание внутренних задач в одном месте и вызов
asyncio.gather
в другом. - Аргумент
aws
, полученныйasyncio.gather
, является фиксированным списком, а это значит, что мы установили количество фоновых задач при вызовеasyncio.gather
, и их нельзя добавлять случайным образом по пути в ожидание. - Когда задача, ожидающая в
asyncio.gather
, выдает исключение, она не может отменить другие выполняемые задачи, что может привести к бесконечному запуску некоторых задач в фоновом режиме и ложному завершению программы.
Поэтому функция структурированного параллелизма, представленная в Python 3.11, является отличным решением наших проблем с параллелизмом. Это позволяет завершить выполнение связанного асинхронного кода в одном и том же месте, и в то же время он позволяет передавать tg
экземпляров в качестве аргументов фоновым задачам, чтобы новые фоновые задачи, созданные в фоновых задачах, не выскакивали из них. управление текущим жизненным циклом асинхронного контекста.
Таким образом, структурированный параллелизм — это революционное улучшение асинхронности Python.
Сравнение с другими библиотеками, реализующими структурированный параллелизм
Структурированный параллелизм не первый в своем роде в Python 3.11; у нас было несколько пакетов на основе параллелизма, которые прекрасно реализовывали эту функцию до версии 3.11.
1. Детские в Трио
Trio была первой библиотекой, предложившей параллелизм структур в мире Python, и в Trio для достижения цели используется API open_nursery
:
2. create_task_group
в Anyio
Но с появлением официального пакета Python asyncio все больше и больше сторонних пакетов используют asyncio для реализации параллельного программирования. На этом этапе использование Trio неизбежно столкнется с проблемами совместимости.
В этот момент появился Anyio, который претендует на совместимость как с asyncio, так и с Trio. Он также может реализовать структурированный параллелизм через create_task_group
API:
3. Использование quattro в младших версиях Python
Если вы хотите, чтобы ваш код был нативным для Python, чтобы в будущем легко пользоваться удобством асинхронного кода Python 3.11, есть хорошая альтернатива quattro, которая имеет меньше звездочек и менее рискованна.
Заключение
API-интерфейсы TaskGroup и timeout, представленные в Python 3.11, приносят нам официальную функцию структурированного параллелизма.
Благодаря структурированному параллелизму мы можем сделать параллельный программный код более абстрагированным, а программистам будет проще контролировать жизненный цикл фоновых задач, тем самым повышая эффективность программирования и избегая ошибок.
Из-за ограниченного опыта, если в этой статье есть какие-либо упущения о параллельном программировании или структурированном параллелизме, или если у вас есть лучшие предложения, пожалуйста, прокомментируйте. Буду благодарен ответить вам.
В следующей статье мы обсудим лучшие практики параллельного программирования в asyncio с использованием разных API. Вы можете прочитать это здесь: