{ "cells": [ { "cell_type": "markdown", "id": "9d2f693b", "metadata": {}, "source": [ "# Работа с asyncio #" ] }, { "cell_type": "markdown", "id": "7501f8e8", "metadata": {}, "source": [ "На этой лекции мы продолжим изучать фреймворк `asyncio`, и мы поговорим о самых важных кирпичиках, которые вы будете использовать для написания программ с использованием библиотеки `asyncio`.\n", "\n", "Мы обсудим на этой лекции, что такое `asyncio.Future`, поговорим о том, как создавать объекты типа `asyncio.Task`. Также мы рассмотрим проблему запуска синхронных функций в цикле обработки событий и немного обсудим библиотеки, которые существуют уже для работы с фреймворком `asyncio`.\n", "\n", "Давайте перейдем к примеру." ] }, { "cell_type": "code", "execution_count": 1, "id": "f5b9ac17", "metadata": {}, "outputs": [], "source": [ "### asyncio.Future, аналог concurrent.futures.Future\n", "import asyncio\n", "\n", "async def slow_operation(future):\n", " await asyncio.sleep(1)\n", " future.set_result(\"Future is done!\")" ] }, { "cell_type": "code", "execution_count": 2, "id": "aaca92a4", "metadata": {}, "outputs": [ { "data": { "text/plain": [ ":4>>" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "loop = asyncio.get_event_loop()\n", "future = asyncio.Future()\n", "asyncio.ensure_future(slow_operation(future))" ] }, { "cell_type": "code", "execution_count": null, "id": "dd778f01", "metadata": {}, "outputs": [], "source": [ "loop.run_until_complete(future)\n", "print(future.result())" ] }, { "cell_type": "code", "execution_count": null, "id": "1f32706e", "metadata": {}, "outputs": [], "source": [ "loop.close()" ] }, { "cell_type": "markdown", "id": "969c9a2f", "metadata": {}, "source": [ "На слайде показан пример функции, которая называется `slow_operation` — это наша корутина, которую мы объявили. Мы в нее передаем некий объект `future`. `asyncio.Future` — это такой объект, который исполняется, и его выполнение еще не завершено. \n", "\n", "Этот объект, целиком и полностью, его интерфейс соответствует объекту `concurrent.futures.Future`. Мы разбирали пример работы с этим объектом, когда знакомились с потоками, и исполняли код при помощи `ThreadPoolExecutor` объекта. Давайте разберем, что делается в этом примере. Мы объявили некую функцию, передали в нее наш созданный объект `future`, выполнили `sleep` на одну секунду, и после этого при помощи `set_result` выставили результат в наш объект типа `future`.\n", "\n", "Обратите внимание - в основной программе мы создаем этот объект, далее мы создаем нашу корутину создаем нашу корутину при помощи `ensure_future`, а в основном цикле обработки событий мы ожидаем завершения нашего объекта `future`, не этой функции, которая называется `slow_operation`, а именно нашего объекта `future`.\n", "\n", "Таким образом, при помощи объектов класса `future` можно выстраивать цепочки не только из двух объектов, но и более сложные цепочки, и очень удобно дожидаться завершения выполнения всех объектов. `event loop asyncio` сам исполнит нужный код и вернет нам результаты.\n", "\n", "Давайте посмотрим, как работает пример в консоли. Переключимся в консоль. Для этого нам понадобится наш пример. Так он выглядит. Давайте запустим его при помощи команды python3." ] }, { "cell_type": "code", "execution_count": 1, "id": "d3c131b7", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Future is done!\r\n" ] } ], "source": [ "! python ex8.py" ] }, { "cell_type": "markdown", "id": "d5e61af6", "metadata": {}, "source": [ "Как мы видим, наша функция успешно отработала, и мы дождались выполнения нашей созданной `future`, или нашего созданного объекта класса `Future`.\n", "\n", "Давайте рассмотрим следующий пример и посмотрим, как можно запустить несколько корутин в одном `event loop`." ] }, { "cell_type": "code", "execution_count": null, "id": "de398fa0", "metadata": {}, "outputs": [], "source": [ "### asyncio.Task, запуск нескольких корутин\n", "import asyncio\n", "\n", "async def sleep_task(num):\n", " for i in range(5):\n", " print(f\"process task: {num} iter: {i}\")\n", " await asyncio.sleep(1)\n", " \n", " return num\n", "\n", "# ensure_future or create_task" ] }, { "cell_type": "code", "execution_count": null, "id": "6052cfbb", "metadata": {}, "outputs": [], "source": [ "loop = asyncio.get_event_loop()\n", "task_list = [loop.create_task(sleep_task(i)) for i in range(2)]\n", "loop.run_until_complete(asyncio.wait(task_list))" ] }, { "cell_type": "code", "execution_count": null, "id": "2c519ebb", "metadata": {}, "outputs": [], "source": [ "loop.run_until_complete(loop.create_task(sleep_task(3)))\n", "loop.run_until_complete(asyncio.gather(\n", " sleep_task(10),\n", " sleep_task(20),\n", "))" ] }, { "cell_type": "markdown", "id": "e0c1da88", "metadata": {}, "source": [ "Для этого, как правило, используется объект типа `asyncio.Task`. `asyncio.Task` является наследником класса `asyncio.Future`, и у него существует ряд дополнительных методов. Со всеми этими методами вы можете ознакомиться в документации, но мы рассмотрим лишь базовый принцип того, как создавать таски и как с ними работать.\n", "\n", "Итак, напрямую объект типа `asyncio.Task` создавать не нужно. Нужно использовать метод `create_task` из вашего `event loop` и передавать в него корутину. То есть, у каждого объекта типа `Task` есть собственная корутина, которую он внутри исполняет. \n", "\n", "Итак, мы создаем список из двух тасков. Запоминаем его в виде списка объектов, и далее при помощи метода `asyncio.wait` мы исполняем список наших тасков в нашем `event loop`.\n", "\n", "Обратите внимание, что можно исполнять как список тасков, так и отдельный таск. Например, если мы исполним код, который я сейчас выделил, то не нужно никаких дополнительных функций вида `asyncio.wait`. Также существует более удобная обертка для исполнения списка тасков — это `asyncio.gather`.\n", "\n", "Давайте рассмотрим, как этот пример будет работать на самом деле в консоли. Переключимся в консоль. Итак, нам нужен код. Давайте исполним его и посмотрим, как на самом деле работает наша корутина. Запускаем при помощи команды python3." ] }, { "cell_type": "code", "execution_count": 2, "id": "3556b3f6", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "process task: 0 iter: 0\n", "process task: 1 iter: 0\n", "process task: 0 iter: 1\n", "process task: 1 iter: 1\n", "process task: 0 iter: 2\n", "process task: 1 iter: 2\n", "process task: 0 iter: 3\n", "process task: 1 iter: 3\n", "process task: 0 iter: 4\n", "process task: 1 iter: 4\n", "process task: 3 iter: 0\n", "process task: 3 iter: 1\n", "process task: 3 iter: 2\n", "process task: 3 iter: 3\n", "process task: 3 iter: 4\n", "process task: 20 iter: 0\n", "process task: 10 iter: 0\n", "process task: 20 iter: 1\n", "process task: 10 iter: 1\n", "process task: 20 iter: 2\n", "process task: 10 iter: 2\n", "process task: 20 iter: 3\n", "process task: 10 iter: 3\n", "process task: 20 iter: 4\n", "process task: 10 iter: 4\n" ] } ], "source": [ "! python ex9.py" ] }, { "cell_type": "markdown", "id": "d8e97341", "metadata": {}, "source": [ "Обратите внимание на то, какой вывод сейчас у нас на экране присутствует. Хочу обратить ваше внимание, что у нас внутри корутины работают последовательно, но все эти корутины исполняются одновременно. Мы видим, что у нас сначала один таск выполняет нулевую итерацию, затем второй таск с номером 1 выполняет свою нулевую итерацию. И затем по очереди нулевой таск выполняет первую итерацию, первый таск выполняет первую итерацию, то есть наши функции, наши корутины, исполняются в `event loop`'е одновременно, а код при этом мы пишем последовательный.\n", "\n", "Это очень удобно, код выглядит достаточно просто. Он похож на исполнение потоков, но на самом деле он выполняется последовательно.\n", "\n", "Давайте еще раз переключимся в консоль и посмотрим на различные методы работы с тасками в `asyncio`." ] }, { "cell_type": "code", "execution_count": null, "id": "4df9ac92", "metadata": {}, "outputs": [], "source": [ "import asyncio\n", "\n", "async def sleep_task(num):\n", " for i in range(5):\n", " print(f\"process task: {num} iter: {i}\")\n", " await asyncio.sleep(1)\n", " \n", " return num\n", "\n", "loop = asyncio.get_event_loop()\n", "\n", "loop.run_until_complete(loop.create_task(sleep_task(3)))" ] }, { "cell_type": "markdown", "id": "624350e4", "metadata": {}, "source": [ "Так, нам еще раз понадобится наш пример. Запускаем интерпретатор python3. Давайте запустим отдельный таск в нашем `event loop`. Делается это достаточно просто - выполняем метод `loop.create_task`, тем самым создавая таск и добавляя его в наш `event loop`, и далее выполняем метод `run_until_complete`. Этот вызов должен нам вернуть результаты выполнения нашего таска.\n", "\n", "Да, действительно, мы видим результат выполнения нашего таска — это число 3.\n", "\n", "Давайте попробуем выполнить несколько тасков при помощи удобной функции `asyncio.gather`. Делается это точно так же — мы запускаем `loop.run_until_complete`, передаем туда удобную функцию `asyncio.gather` и в этой функции перечисляем список тасков.\n", "\n", "Хочу обратить внимание, в чем отличие. Нам не нужно здесь вызывать метод `create_task`, все внутри будет автоматически вызвано, не нужно запоминать список наших тасков. Вся эта конструкция вернет результаты выполнения. Давайте проверим. Итак, ожидаем завершения наших тасков." ] }, { "cell_type": "code", "execution_count": null, "id": "ab6ccef9", "metadata": {}, "outputs": [], "source": [ "rsp = loop.run_until_complete(asyncio.gather(sleep_task(4), sleep_task(5)))" ] }, { "cell_type": "markdown", "id": "622fc023", "metadata": {}, "source": [ "Мы опять видим, что таски хоть и работают последовательно, но запускаются они одновременно и пока один таск выполняет `sleep`, второй продолжает работать и так далее." ] }, { "cell_type": "code", "execution_count": null, "id": "9c4f6544", "metadata": {}, "outputs": [], "source": [ "rsp" ] }, { "cell_type": "markdown", "id": "dfef2909", "metadata": {}, "source": [ "Итак, мы получили ответ. И, действительно, это массив, который содержит результаты работы двух наших тасков. \n", "\n", "Давайте двигаться дальше, и мне хотелось бы остановить ваше внимание на том, как исполнить синхронную функцию в нашем `event loop`. Как правило, таких задач не должно возникать, но если вдруг они и возникли, то они будут представлять из себя небольшую сложность.\n", "\n", "Хочу объяснить, почему. Так как наш `event loop`, или цикл обработки событий, постоянно переключает контекст, и переключает контекст между всеми нашими корутинами и их исполняет последовательно, пока одна корутина ожидает ввода-вывода, вторую корутину наш `event loop` благополучно исполняет. Если код, который будет исполняться в корутине, будет блокирующим, то наш `event loop` не сможет делать переключения контекста, и это будет очень плохо сказываться на вообще всем нашем коде, и с этим нужно что-то делать.\n", "\n", "Как раз для этого в `asyncio` существует метод `run_in_executor`. Он означает запустить код буквально в пуле потоков, который внутри этого `event loop`'а автоматически будет создан. Можно использовать и собственный пул потоков, а можно использовать уже готовый по умолчанию. Более подробную информацию можно получить в документации, а на этом примере можно наблюдать, как функция `urlopen`, которая открывает некий `url`, который ей передали по `http`, скачивает результаты в отдельном потоке и мы дожидаемся результатов выполнения этой функции, опять же при помощи механизма `event loop`, который называется `futures`. " ] }, { "cell_type": "code", "execution_count": null, "id": "5a18a7bd", "metadata": {}, "outputs": [], "source": [ "# loop.run_in_executor, запуск в отдельном потоке\n", "import asyncio\n", "from urllib.request import urlopen\n", "\n", "# a synchronous function\n", "def sync_get_url(url):\n", " return urlopen(url).read()\n", "\n", "async def load_url(url, loop=None):\n", " future = loop.run_in_executor(None, sync_get_url, url)\n", " response = await future\n", " print(len(response))\n", " \n", "loop = asyncio.get_event_loop()\n", "loop.run_until_complete(load_url(\"http://vniitf.ru/\", loop=loop))" ] }, { "cell_type": "markdown", "id": "09defb3d", "metadata": {}, "source": [ "Давайте посмотрим, как он выполняется. Итак, мы вызываем метод `run_in_executor`. Здесь внутри будет создано нужное количество потоков, и наша функция `sync_get_url` с параметром `url` будет выполнена в отдельном потоке. Для того, чтобы дождаться результата выполнения нашей функции в отдельном потоке, мы используем конструкцию `await` и передаем туда объект `future`.\n", "\n", "Давайте посмотрим, как этот код будет работать в консоли. Итак, нам нужен код нашего примера. Давайте еще раз на него взглянем. Мы будем открывать страничку vniitf.ru и выводить на экран количество байт, которые она занимает. Итак, выполняем наш пример." ] }, { "cell_type": "code", "execution_count": 3, "id": "356db6d9", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "97427\r\n" ] } ], "source": [ "! python ex10.py" ] }, { "cell_type": "markdown", "id": "afe9e38e", "metadata": {}, "source": [ "Мы видим на экране 95 килобайт. Мы исполнили сейчас функцию `urlopen` в отдельном потоке, и в нашем `event loop` мы дождались результатов выполнения этой функции, которая работала синхронно. Как правило, такие задачи будут возникать у вас, если в некоторой библиотеке не будет поддержки работы с `asyncio`. Это может сказаться негативно, как я уже сказал, на производительности, потому что, как мы помним, потоки в Python запускаются и работают с ограничением `GIL`, и будет лучше, если ваш код будет работать без этих вещей.\n", "\n", "Хочу остановить ваше внимание на том, что сейчас уже есть достаточно большое количество библиотек, которые работают с `asyncio`, где можно посмотреть документацию по ним и получить информацию, как их использовать. Я привел ссылку на слайде, это на `https://github.com/aio-libs`. Там существует достаточно большой список этих библиотек. Самая известная библиотека — это `aiohttp - https://github.com/aio-libs/aiohttp`. При помощи нее можно делать `http` запросы в вашем коде и выполнять эти запросы без `ThreadPoolExecutor`'а, а напрямую в корутинах. `aiomysql - https://github.com/aio-libs/aiomysql` — это библиотека для работы с популярной базой `mysql`, `aiomcache - https://github.com/aio-libs/aiomcache`, и так далее.\n", "\n", "Таких библиотек появляется в последнее время все больше и больше, и это радует, так как все это обеспечит хорошее качество написания кода для работы с библиотекой `asyncio`.\n", "\n", "Итак, на этой лекции мы рассмотрели основные приемы для написания кода с использованием библиотеки `asyncio`. Мы поговорили о том, что такое `asyncio.Future` объект. Мы поговорили о том, как можно запустить несколько объектов типа `asyncio.Task` и посмотреть, как они исполняются в `event loop`'е, а также обсудили проблему работы с синхронными функциями, какие приемы для этого использовать. Все эти подходы и проблемы, которые мы обсудили, понадобятся вам в дальнейшем для написания заданий и самостоятельного изучения библиотеки `asyncio`." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.8" } }, "nbformat": 4, "nbformat_minor": 5 }