Параллелизм Python

Почему Taskgroup и тайм-аут так важны в Python 3.11 asyncio

Использование структурированного параллелизма в Python 3.11

Это мой первый пост в моей колонке Python Concurrency, и если вы сочтете его полезным, вы можете прочитать остальные здесь.

Новые функции пакета asyncio Python 3.11

1. Введение

В прошлогоднем выпуске Python 3.11 в пакет asyncio были добавлены TaskGroup и timeoutAPI. Эти два 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. Вы можете прочитать это здесь: