asyncio и блокирующий код
Реализация асинхронности в Python с модулем asyncio
Асинхронное программирование — это особенность современных языков программирования, которая позволяет выполнять операции, не дожидаясь их завершения. Асинхронность — одна из важных причин популярности Node.js.
Представьте приложение для поиска по сети, которое открывает тысячу соединений. Можно открывать соединение, получать результат и переходить к следующему, двигаясь по очереди. Однако это значительно увеличивает задержку в работе программы. Ведь открытие соединение — операция, которая занимает время. И все это время последующие операции находятся в процессе ожидания.
А вот асинхронность предоставляет способ открытия тысячи соединений одновременно и переключения между ними. По сути, появляется возможность открыть соединение и переходить к следующему, ожидая ответа от первого. Так продолжается до тех пор, пока все не вернут результат.
На графике видно, что синхронный подход займет 45 секунд, в то время как при использовании асинхронности время выполнения можно сократить до 20 секунд.
Где асинхронность применяется в реальном мире?
Асинхронность больше всего подходит для таких сценариев:
Разница в понятиях параллелизма, concurrency, поточности и асинхронности
Параллелизм — это выполнение нескольких операций за раз. Многопроцессорность — один из примеров. Отлично подходит для задач, нагружающих CPU.
Concurrency — более широкое понятие, которое описывает несколько задач, выполняющихся с перекрытием друг друга.
Поточность — поток — это отдельный поток выполнения. Один процесс может содержать несколько потоков, где каждый будет работать независимо. Отлично подходит для IO-операций.
Асинхронность — однопоточный, однопроцессорный дизайн, использующий многозадачность. Другими словами, асинхронность создает впечатление параллелизма, используя один поток в одном процессе.
Составляющие асинхронного программирования
Разберем различные составляющие асинхронного программирования подробно. Также используем код для наглядности.
Сопрограммы
Сопрограммы (coroutine) — это обобщенные формы подпрограмм. Они используются для кооперативных задач и ведут себя как генераторы Python.
Пример сопрограммы
Асинхронный Python: различные формы конкурентности
Определение терминов:
Прежде чем мы углубимся в технические аспекты, важно иметь некоторое базовое понимание терминов, часто используемых в этом контексте.
Синхронный и асинхронный:
В синхронных операциях задачи выполняются друг за другом. В асинхронных задачи могут запускаться и завершаться независимо друг от друга. Одна асинхронная задача может запускаться и продолжать выполняться, пока выполнение переходит к новой задаче. Асинхронные задачи не блокируют (не заставляют ждать завершения выполнения задачи) операции и обычно выполняются в фоновом режиме.
Например, вы должны обратиться в туристическое агентство, чтобы спланировать свой следующий отпуск. Вам нужно отправить письмо своему руководителю, прежде чем улететь. В синхронном режиме, вы сначала позвоните в туристическое агентство, и если вас попросят подождать, то вы будете ждать, пока вам не ответят. Затем вы начнёте писать письмо руководителю. Таким образом, вы выполняете задачи последовательно, одна за одной. [синхронное выполнение, прим. переводчика] Но, если вы умны, то пока вас попросили подождать [повисеть на телефоне, прим. переводчика] вы начнёте писать e-mail и когда с вами снова заговорят вы приостановите написание, поговорите, а затем допишете письмо. Вы также можете попросить друга позвонить в агентство, а сами написать письмо. Это асинхронность, задачи не блокируют друг друга.
Конкурентность и параллелизм:
Конкурентность подразумевает, что две задачи выполняются совместно. В нашем предыдущем примере, когда мы рассматривали асинхронный пример, мы постепенно продвигались то в написании письма, то в разговоре с тур. агентством. Это конкурентность.
Когда мы попросили позвонить друга, а сами писали письмо, то задачи выполнялись параллельно.
Параллелизм по сути является формой конкурентности. Но параллелизм зависит от оборудования. Например, если в CPU только одно ядро, то две задачи не могут выполняться параллельно. Они просто делят процессорное время между собой. Тогда это конкурентность, но не параллелизм. Но когда у нас есть несколько ядер [как друг в предыдущем примере, который является вторым ядром, прим. переводчика] мы можем выполнять несколько операций (в зависимости от количества ядер) одновременно.
Потоки и процессы
Python поддерживает потоки уже очень давно. Потоки позволяют выполнять операции конкурентно. Но есть проблема, связанная с Global Interpreter Lock (GIL) из-за которой потоки не могли обеспечить настоящий параллелизм. И тем не менее, с появлением multiprocessing можно использовать несколько ядер с помощью Python.
Рассмотрим небольшой пример. В нижеследующем коде функция worker будет выполняться в нескольких потоках асинхронно и одновременно.
А вот пример выходных данных:
Таким образом мы запустили 5 потоков для совместной работы и после их старта (т.е. после запуска функции worker) операция не ждёт завершения работы потоков прежде чем перейти к следующему оператору print. Это асинхронная операция.
В нашем примере мы передали функцию в конструктор Thread. Если бы мы хотели, то могли бы реализовать подкласс с методом (ООП стиль).
Чтобы узнать больше о потоках, воспользуйтесь ссылкой ниже:
GIL был представлен, чтобы сделать обработку памяти CPython проще и обеспечить наилучшую интеграцию с C(например, с расширениями). GIL — это механизм блокировки, когда интерпретатор Python запускает в работу только один поток за раз. Т.е. только один поток может исполняться в байт-коде Python единовременно. GIL следит за тем, чтобы несколько потоков не выполнялись параллельно.
Краткие сведения о GIL:
Эти ресурсы позволят углубиться в GIL:
Чтобы достичь параллелизма в Python был добавлен модуль multiprocessing, который предоставляет API, и выглядит очень похожим, если вы использовали threading раньше.
Давайте просто пойдем и изменим предыдущий пример. Теперь модифицированная версия использует Процесс вместо Потока.
Что же изменилось? Я просто импортировал модуль multiprocessing вместо threading. А затем, вместо потока я использовал процесс. Вот и всё! Теперь вместо множества потоков мы используем процессы которые запускаются на разных ядрах CPU (если, конечно, у вашего процессора несколько ядер).
С помощью класса Pool мы также можем распределить выполнение одной функции между несколькими процессами для разных входных значений. Пример из официальных документов:
Здесь вместо того, чтобы перебирать список значений и вызывать функцию f по одному, мы фактически запускаем функцию в разных процессах. Один процесс выполняет f(1), другой-f(2), а другой-f (3). Наконец, результаты снова объединяются в список. Это позволяет нам разбить тяжелые вычисления на более мелкие части и запускать их параллельно для более быстрого расчета.
Модуль concurrent.futures большой и позволяет писать асинхронный код очень легко. Мои любимчики ThreadPoolExecutor и ProcessPoolExecutor. Эти исполнители поддерживают пул потоков или процессов. Мы отправляем наши задачи в пул, и он запускает задачи в доступном потоке / процессе. Возвращается объект Future, который можно использовать для запроса и получения результата по завершении задачи.
А вот пример ThreadPoolExecutor:
У меня есть статья о concurrent.futures masnun.com/2016/03/29/python-a-quick-introduction-to-the-concurrent-futures-module.html. Она может быть полезна при более глубоком изучении этого модуля.
Asyncio — что, как и почему?
У вас, вероятно, есть вопрос, который есть у многих людей в сообществе Python — что asyncio приносит нового? Зачем нужен был еще один способ асинхронного ввода-вывода? Разве у нас уже не было потоков и процессов? Давай посмотрим!
Зачем нам нужен asyncio?
Процессы очень дорогостоящие [с точки зрения потребления ресурсов, прим. переводчика] для создания. Поэтому для операций ввода/вывода в основном выбираются потоки. Мы знаем, что ввод-вывод зависит от внешних вещей — медленные диски или неприятные сетевые лаги делают ввод-вывод часто непредсказуемым. Теперь предположим, что мы используем потоки для операций ввода-вывода. 3 потока выполняют различные задачи ввода-вывода. Интерпретатор должен был бы переключаться между конкурентными потоками и давать каждому из них некоторое время по очереди. Назовем потоки — T1, T2 и T3. Три потока начали свою операцию ввода-вывода. T3 завершает его первым. T2 и T1 все еще ожидают ввода-вывода. Интерпретатор Python переключается на T1, но он все еще ждет. Хорошо, интерпретатор перемещается в T2, а тот все еще ждет, а затем перемещается в T3, который готов и выполняет код. Вы видите в этом проблему?
T3 был готов, но интерпретатор сначала переключился между T2 и T1 — это понесло расходы на переключение, которых мы могли бы избежать, если бы интерпретатор сначала переключился на T3, верно?
Asyncio предоставляет нам цикл событий наряду с другими крутыми вещами. Цикл событий (event loop) отслеживает события ввода/вывода и переключает задачи, которые готовы и ждут операции ввода/вывода [цикл событий — программная конструкция, которая ожидает прибытия и производит рассылку событий или сообщений в программе, прим. переводчика].
Идея очень проста. Есть цикл обработки событий. И у нас есть функции, которые выполняют асинхронные операции ввода-вывода. Мы передаем свои функции циклу событий и просим его запустить их для нас. Цикл событий возвращает нам объект Future, словно обещание, что в будущем мы что-то получим. Мы держимся за обещание, время от времени проверяем, имеет ли оно значение (нам очень не терпится), и, наконец, когда значение получено, мы используем его в некоторых других операциях [т.е. мы послали запрос, нам сразу дали билет и сказали ждать, пока придёт результат. Мы периодически проверяем результат и как только он получен мы берем билет и по нему получаем значение, прим. переводчика].
Asyncio использует генераторы и корутины для остановки и возобновления задач. Прочитать детали вы можете здесь:
Прежде чем мы начнём, давайте взглянем на пример:
Обратите внимание, что синтаксис async/await предназначен только для Python 3.5 и выше. Пройдёмся по коду:
Делаем правильный выбор
Только что мы прошлись по самым популярным формам конкурентности. Но остаётся вопрос — что следует выбрать? Это зависит от вариантов использования. Из моего опыта я склонен следовать этому псевдо-коду:
Обзор Async IO в Python 3.7
Перевод обзорной статьи: Guest Contributor Overview of Async IO in Python 3.7
Будет полезна для введения в модуль asyncio
Модуль Python 3 asyncio предоставляет фундаментальные инструменты для реализации асинхронного ввода-вывода в Python. Он был представлен в Python 3.4, и с каждым последующим релизом модуль развивался.
Это статья содержит общий обзор асинхронной парадигмы и того, как она реализована в Python 3.7.
Блокирующий и неблокирующий ввод/вывод
Проблема, которую пытается решить асинхронностью, — это блокировка ввода-вывода.
По умолчанию, когда ваша программа обращается к данным из источника ввода-вывода, она ожидает завершения этой операции, прежде чем продолжить выполнение программы.
Программа заблокирована от продолжения выполнения во время доступа к физическому устройству и передачи данных.
Еще одним распространенным примером блокировки являются сетевые операции:
Во многих случаях задержка, вызванная блокировкой, незначительна. Однако блокировка ввода/вывода очень плохо масштабируется. Если вам нужно дождаться чтения файла 1010 или сетевых транзакций, производительность заметно снизится.
Многопроцессорность, многопоточность и асинхронность
Стратегии минимизации задержек блокирования ввода-вывода делятся на три основные категории: многопроцессорная обработка (multiprocessing), многопоточность (threading) и асинхронность.
Многопроцессорная обработка
Многопроцессорная обработка — это форма параллельных вычислений при котором инструкции выполняются в перекрывающихся временных рамках на нескольких физических процессорах или ядрах. Каждый процесс, порожденный ядром, несет накладные расходы, включая независимо выделенный кусок памяти (heap).
Python реализует такой параллелизм с помощью модуля multiprocessing.
Ниже приведен пример программы на Python 3, которая порождает четыре дочерних процесса, каждый из которых имеет случайную независимую задержку. Выходные данные показывают идентификатор процесса каждого дочернего элемента, системное время до и после каждой задержки, а также текущее и пиковое распределение памяти на каждом шаге.
Результат выполнения:
Многопоточность
Потоки являются альтернативой многопроцессорности, с преимуществами и недостатками.
Потоки независимо планируются, и их выполнение может происходить в течение перекрывающегося периода времени. Однако, в отличие от многопроцессорной обработки, потоки существуют полностью в одном процессе ядра и совместно используют одну выделенную память (heap).
Потоки Python являются параллельными (concurrent) — несколько последовательностей машинного кода выполняются в перекрывающихся временных рамках. Но в реальности они не параллельны — выполнение не происходит одновременно на нескольких физических ядрах.
Основными недостатками потоков Python являются безопасность памяти (memory safety) и состояние гонки (race conditions). Все дочерние потоки родительского процесса работают в одном и том же пространстве общей памяти. Без дополнительных средств защиты один поток может перезаписать общее значение в памяти, и другие потоки об этом не узнают. Такое повреждение данных будет иметь катастрофические последствия.
Для обеспечения безопасности потоков в реализации CPython используют глобальную блокировку интерпретатора (GIL). GIL — это мьютексный механизм, который предотвращает одновременное выполнение нескольких потоков на объектах Python. Фактически это означает, что в любой момент времени выполняется только один поток.
Вот потоковая версия примера многопроцессорной обработки из предыдущего раздела. Обратите внимание, что очень мало что изменилось: multiprocessing.Process заменен на threading.Thread. Как указано в выходных данных, все происходит за один процесс, и объем памяти значительно уменьшается.
Результат выполнения:
Асинхронность
Асинхронность является альтернативой многопоточности для написания параллельных приложений. Асинхронные события происходят независимо друг от друга (не синхронизированно друг с другом), полностью в одном потоке.
В отличие от многопоточности, в асинхронных программах программист контролирует, когда и как происходит произвольное вытеснение, облегчая изоляцию и избегая условий гонки.
Введение в модуль Python 3.7 asyncio
В Python 3.7 асинхронные операции предоставляются модулем asyncio.
High-Level против Low-Level asyncio API
Компоненты Asyncio подразделяются на API-интерфейсы высокого уровня (для написания программ) и API-интерфейсы низкого уровня (для написания библиотек или сред на основе asyncio).
Каждая программа asyncio может быть написана с использованием только высокоуровневых API. Если вы не пишете фреймворк или библиотеку, вам никогда не нужно трогать API низкого уровня.
С учетом вышесказанного давайте рассмотрим основные высокоуровневые API и обсудим основные концепции.
Корутины (Coroutines)
В общем, coroutine (сокращение от cooperative subroutine) — это подпрограмма, предназначенная для добровольной упреждающей многозадачности: она активно уступает свои ресурсы другим подпрограммам и процессам, а не принудительно вытесняется ядром. Термин «coroutine» был придуман в 1958 году Мелвином Конвеем (в Conway’s Law), чтобы описать код, который сам освобождает свои ресурсы для других частей системы.
В asyncio это так же называется awaiting.
Awaitables, Async, и Await
Любой объект, который можно ожидать прерывание своего процесса выполнения, называется awaitable.
Ключевое слово await приостанавливает выполнение текущей подпрограммы (coroutine) и вызывает указанное ожидание awaitable.
В Python 3.7 есть три ожидаемых объекта (awaitable) — coroutine, task и future.
Coroutine в asyncio — это любая функция Python, в определении которой указан префикс async.
task в asyncio — это объект, который оборачивает coroutine, предоставляя методы для контроля ее выполнения и запроса ее статуса. task может быть создан с помощью asyncio.create_task() или asyncio.gather().
future в asyncio — это низкоуровневый объект, который выполняет роль заполнителя для данных, которые еще не были рассчитаны или получены. Он может обеспечить пустую структуру для последующего заполнения данными и механизм обратного вызова, который срабатывает, когда данные готовы.
Обычно в Python 3.7 вам никогда не нужно напрямую создавать низкоуровневый объект future.
Event Loops
В asyncio event loop (цикл обработки событий) управляет планированием и передачей ожидаемых объектов. event loop требуется для использования awaitables. Каждая программа asyncio имеет как минимум один event loop. Можно иметь несколько event loop, но в Python 3.7 настоятельно рекомендуется использовать только один event loop.
Ссылка на работающий в данный момент объект цикла получается путем вызова asyncio.get_running_loop().
Sleeping
Подпрограмма asyncio.sleep(delay) блокируется на секунды задержки. Это используется для имитации блокировки ввода-вывода.
Инициализация главного Event Loop
Канонической точкой входа в программу asyncio является asyncio.run(main()), где main () — подпрограмма (coroutine) верхнего уровня.
Вызов asyncio.run() неявно создает и запускает event loop (цикл обработки событий). У объекта цикла есть много полезных методов, включая loop.time(), который возвращает число с плавающей запятой, представляющее текущее время, измеренное внутренними часами цикла.
Примечание. Функция asyncio.run() не может быть вызвана из существующего цикла событий. Следовательно, возможно, что вы увидите ошибки, если вы запускаете программу в контролирующей среде, такой как Anaconda или Jupyter, которая выполняет собственный цикл обработки событий. Примеры программ в этом разделе и следующих разделах должны запускаться непосредственно из командной строки путем выполнения файла python.
Следующая программа печатает строки текста, блокируясь на одну секунду после каждой строки.
Результат выполнения:
Задачи (Task)
Задача — это awaitable (ожидаемый) объект, который оборачивается вокруг подпрограммы (coroutine). Чтобы создать и сразу запланировать задачу, вы можете вызвать следующее:
Этот код вернет объект задачи. Создание задачи говорит циклу: «Иди и запусти эту coroutine (подпрограмму), как только сможешь».
Если вы ожидаете (await) задачу, выполнение текущей coroutine блокируется, пока эта задача не будет завершена.
Результат выполнения:
Задачи имеют несколько полезных методов для управления подпрограммами (coroutine). В частности, вы можете запросить отмену задачи, вызвав метод .cancel(). Задача будет запланирована для отмены в следующем проходе цикла событий. Отмена не гарантируется: задание может быть выполнено до прохода цикла, и в этом случае отмена не будет.
Сбор объектов Awaitable
Объекты awaitable могут быть собраны в группу, с помощью команды asyncio.gather(awaitables).
Asyncio.gather() возвращает объект awaitable, представляющее собранные awaitable значения.
Сбор — это удобный способ запланировать одновременное выполнение нескольких подпрограмм в качестве задач. Он также связывает собранные задачи несколькими полезными способами:
Пример: асинхронные веб-запросы с aiohttp
В следующем примере показано, как можно реализовать это высокоуровневое асинхронное API. Ниже приведена измененная версия, обновленная для Python 3.7, примера Asyncio Скотта Робинсона (Scott Robinson’s nifty asyncio). Его программа использует модуль aiohttp для захвата верхних постов в Reddit и вывода их на консоль.
Убедитесь, что у вас установлен модуль aiohttp, прежде чем запускать скрипт ниже. Вы можете установить модуль с помощью следующей команды pip:
Другое высокоуровневое API
Надеемся, что этот обзор даст вам основу понимания того, как, когда и зачем использовать asyncio. Другие высокоуровневые API-интерфейсы asyncio, которые здесь не рассматриваются, включают в себя:
Заключение
Имейте в виду, что даже если ваша программа не требует асинхронности, вы все равно можете использовать asyncio, по соображениям производительности. Я надеюсь, что этот обзор даст вам четкое представление о том, как, когда и почему начать использовать asyncio.
Что внутри asyncio
В этой статье я предлагаю читателю совершить со мной в меру увлекательное путешествие в недра asyncio, чтобы разобраться, как в ней реализовано асинхронное выполнение кода. Мы оседлаем коллбэки и промчимся по циклу событий сквозь пару ключевых абстракций прямо в корутину. Если на вашей карте питона еще нет этих достопримечательностей, добро пожаловать под кат.
Для затравки — краткая справка о раскинувшейся перед нами местности
asyncio — библиотека асинхронного ввода/вывода которая, согласно pep3153, была создана, чтобы предоставить стандартизованную базу для создания асинхронных фреймворков. pep3156 так же приписывает ей необходимость обеспечить предельно простую интеграцию в уже существовавшие асинхронные фреймворки (Twisted, Tornado, Gevent). Как мы можем сейчас наблюдать, эти цели были успешно достигнуты — появился новый фреймворк на основе asyncio: aiohttp, в Tornado AsyncioMainLoop является циклом событий по умолчанию с версии 5.0, в Twisted asyncioreactor доступен с версии 16.5.0, а для Gevent есть сторонняя библиотека aiogevent.
asyncio — это гибридная библиотека, использующая одновременно два подхода к реализации асинхронного выполнения кода: классический на коллбэках и, относительно новый, (по крайней мере для питона) на корутинах. В её основе лежат три основные абстракции, являющиеся аналогами абстракций, существующих в сторонних фреймворках:
Поехали!
Цикл событий — основная составляющая библиотеки, по дорогам, пролегающим через него, данные доставляются в любые её компоненты. Он большой и сложный, поэтому сначала рассмотрим его урезанный вариант.
Оседлав наш маленький коллбэк, мы отправляемся в путь через call_soon, попадаем в очередь и после краткого ожидания будем выведены на экран.
Эпизод про плохие коллбэки
Стоит упомянуть, что коллбэки это опасные лошадки — если они сбросят вас посреди дороги, интерпретатор питона не сможет помочь понять, где это произошло. Если не верите, покатайтесь той же дорогой на коллбэке maybe_print, приходящем к финишу примерно в половине случаев.
Ниже показан полный трейсбэк запуска предыдущего примера. Из-за того, что функция maybe_print была запущена циклом событий, а не напрямую из starting_point, трейсбэк заканчивается именно на нём, в методе run_until_complete. По такому трейсбэку невозможно определить, где в коде находится starting_point, что значительно усложнит отладку, если starting_point будут находиться в нескольких местах кодовой базы.
Непрерывный стек вызовов нужен не только для вывода полного трейсбэка, но и для реализации других возможностей языка. Например, на нём основана обработка исключений. Пример ниже не заработает, потому что к моменту запуска starting_point, функция main уже будет выполнена:
Следующий пример тоже не заработает. Менеджер контекста в функции main откроет и закроет файл ещё до того, как будет запущена его обработка.
Отсутствие непрерывного стека вызовов ограничивает использование привычных возможностей языка. Для частичного обхода этого недостатка в asyncio пришлось добавить много дополнительного кода, не относящeгося напрямую к решаемой ей задаче. Этот код, по большей части, отсутствует в примерах — они и без него довольно сложны.
Из цикла событий во внешний мир и обратно
Цикл событий сообщается с внешним миром через операционную систему посредством событий. Код, который умеет с ней работать, предоставляется модулем стандартной библиотеки под названием selectors. Он позволяет сказать операционной системе, что мы ждем какого-то события, а потом спросить, произошло ли оно. В примере ниже ожидаемым событием будет доступность сокета на чтение.
Гонец из внешнего мира оставляет своё сообщение или посылку в селекторе, а селектор передаёт её получателю. Теперь стало возможным читать из сокета, используя цикл событий. Если запустить этот код и подключиться с помощью netcat, то он будет добросовестно выводить всё, что в него будет отправлено.
В начале статьи говорилось, что asyncio — гибридная библиотека, в которой корутины работают поверх коллбэков. Для реализации этой функциональности используются две оставшиеся основные абстракции: Task и Future. Далее будет показан код этих абстракций, а затем, как, используя их цикл событий, выполняются корутины.
Future
Ниже представлен код класса Future. Он нужен для того, чтобы в корутине можно было дождаться завершения выполнения коллбэка и получить его результат.
Это специальный подкласс класса Future. Он нужен для запуска корутины на коллбэчном цикле событий.
Цикл событий, умеющий работать с Future
Двинемся дальше
Теперь проследим за тем, как корутина main будет выполняться:
Вот таким нехитрым способом asyncio выполняет корутины.
Итоги
Один из целей создания asyncio было обеспечить предельно простую интеграцию в уже существовавшие асинхронные фреймворки (Twisted, Tornado, Gevent). Из этой цели логически вытекает выбор инструментов: если бы не было требования совместимости, возможно, корутинам была бы отдана главная роль. Из-за того, что при программировании на коллбэках невозможно сохранить непрерывный стэк вызовов, на границе между ними и корутинами пришлось создать дополнительную систему, обеспечивающую поддержку опирающихся на него возможностей языка.
Теперь главный вопрос. Зачем всё это знать простому пользователю библиотеки, который следует рекомендациям из документации и использует лишь корутины и высокоуровневый API?
Вот кусок документации класса StreamWriter
Его экземпляр возвращается функцией asyncio.open_connection и является async/await API поверх API на коллбэках. И эти коллбэки из него торчат. Функции write и writelines синхронные, они пытаются писать в сокет, а если не получается, то сбрасывают данные в нижележащий буфер и добавляют коллбэки на запись. Корутина drain нужна для того, чтобы обеспечить возможность дождаться, пока количество данных в буфере не опустится до заданного значения.
Если забыть вызвать drain между вызовами write, то внутренний буфер может разрастись до неприличных размеров. Однако, если помнить об этом, то остается пара неприятных моментов. Первый: если коллбэк на запись «сломается», то корутина, использующая этот API никак об этом не узнает и, соответственно, не сможет обработать. Второй: если корутина «сломается», то коллбэк на запись никак об этом не узнает и продолжит писать данные из буфера.
Таким образом, даже используя только корутины, будьте готовы к тому, что коллбэки напомнят о себе.
О том, как работать с базами данных из асинхронного кода, вы можете прочитать в этой статье нашего корпоративного блога Antida software.