{ "cells": [ { "cell_type": "markdown", "id": "d26cab23", "metadata": {}, "source": [ "# Первые шаги с asyncio #" ] }, { "cell_type": "markdown", "id": "116dd167", "metadata": {}, "source": [ "На этой лекции мы начнем свое первое знакомство с фреймворком `asyncio`, и мы разберем некоторые примеры кода с использованием этого фреймворка.\n", "\n", "Прежде чем мы начнем разбирать примеры кода на `asyncio`, хочется про него сказать следующее.\n", "\n", "Во-первых, `asyncio` — это библиотека, которая стала частью Python 3, и она распространяется вместе с самим дистрибутивом Python 3. `аsyncio` отвечает за неблокирующий ввод/вывод, на этом фреймворке можно написать сервис, который работает с десятками тысяч соединений одновременно. В основе работы этого фреймворка лежат генераторы и корутины, о чем мы уже говорили на предыдущих лекциях. И знания о том, как работают генераторы и корутины, помогут вам разобраться и с тем, как устроен фреймворк `asyncio`. В целом, это линейный код, в нем отсутствуют какие-либо `callback`-и. Выглядит он достаточно просто, но внутри устроен не совсем так легко, как кажется.\n", "\n", "Итак, давайте приступим к примерам. На слайде показан пример, в нем мы объявляем функцию, добавляем к этой функции декоратор `asyncio.coroutine`, тем самым делая нашу функцию корутиной." ] }, { "cell_type": "code", "execution_count": 1, "id": "e223ec44", "metadata": {}, "outputs": [], "source": [ "# asyncio, Hello World\n", "import asyncio\n", "\n", "@asyncio.coroutine\n", "def hello_world():\n", " while True:\n", " print(\"Hello World!\")\n", " yield from asyncio.sleep(1.0)" ] }, { "cell_type": "code", "execution_count": null, "id": "2cea80c1", "metadata": {}, "outputs": [], "source": [ "loop = asyncio.get_event_loop()\n", "loop.run_until_complete(hello_world())\n", "loop.close()" ] }, { "cell_type": "markdown", "id": "dcb391dc", "metadata": {}, "source": [ " Далее, мы в бесконечном цикле выполняем вывод строчки \"Hello World\" в консоль и делаем вызов `yield from asyncio.sleep`. Тем самым мы засыпаем на одну секунду. Хочу также обратить ваше внимание, что как раз мы здесь используем не привычный нам `time.sleep` вызов, а именно специальную конструкцию `yield from` для того, чтобы наша корутина приостановила свою работу, тем самым дала возможность поисполняться другим корутинам.\n", " \n", " Весь код в `asyncio` строится на основе понятия цикла обработки событий или, как еще его иногда называют, `event loop`. `event loop` — это своего рода планировщик задач или корутин, которые в нем исполняются. Он отвечает за ввод/вывод, он отвечает за управление сигналами, всеми сетевыми операциями и переключает контекст между всеми корутинами, которые в нем зарегистрированы и выполняются. Если одна корутина ожидает завершения какой-то сетевой операции, например, ждет, пока данные поступят в сокет, то в этот момент `event loop` может переключиться на другую корутину и продолжить ее выполнение.\n", " \n", "Давайте посмотрим далее в пример. При помощи вызова `asyncio.get_event_loop` мы получаем наш цикл обработки событий. Это объект, и мы можем попросить этот объект исполнить нам некоторую корутину. Опять же, хочу обратить ваше внимание, что обычные функции исполнять нельзя, нужно исполнять именно корутины. Давайте посмотрим, как этот пример работает в консоли. Нам потребуется код этого примера. Давайте запустим его при помощи интерпретатора python3. Запускаем." ] }, { "cell_type": "code", "execution_count": 3, "id": "e93cbbd0", "metadata": { "scrolled": true }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Hello World!\n", "Hello World!\n", "Hello World!\n", "^C\n", "Traceback (most recent call last):\n", " File \"ex5.py\", line 11, in \n", " loop.run_until_complete(hello_world())\n", " File \"/usr/lib64/python3.6/asyncio/base_events.py\", line 471, in run_until_complete\n", " self.run_forever()\n", " File \"/usr/lib64/python3.6/asyncio/base_events.py\", line 438, in run_forever\n", " self._run_once()\n", " File \"/usr/lib64/python3.6/asyncio/base_events.py\", line 1415, in _run_once\n", " event_list = self._selector.select(timeout)\n", " File \"/usr/lib64/python3.6/selectors.py\", line 445, in select\n", " fd_event_list = self._epoll.poll(timeout, max_ev)\n", "KeyboardInterrupt\n" ] } ], "source": [ "! python ex5.py" ] }, { "cell_type": "markdown", "id": "77084278", "metadata": {}, "source": [ "Видим на экране ожидаемое поведение, видим вывод строчки \"Hello World\". Для того, чтобы завершить работу с нашим циклом обработки событий, необходимо вызвать метод `close` для этого объекта `loop`." ] }, { "cell_type": "code", "execution_count": null, "id": "24532e1d", "metadata": { "scrolled": true }, "outputs": [], "source": [ "# asyncio, async def / await; PEP 492 Python3.5\n", "import asyncio\n", "\n", "async def hello_world():\n", " while True:\n", " print(\"Hello World!\")\n", " await asyncio.sleep(1.0)\n", " \n", "loop = asyncio.get_event_loop()\n", "loop.run_until_complete(hello_world())\n", "loop.close()" ] }, { "cell_type": "markdown", "id": "a92d9a69", "metadata": {}, "source": [ "Начиная с версии Python 3.5, появился новый `PEP 492`, в котором был введен специальный синтаксис для написания корутин. Это `async def` и `await`. Во-первых, этот синтаксис выглядит более лаконично и красиво по сравнению с предыдущим. Наш пример сейчас стал занимать меньше строчек, но также он содержит еще в себе и другие важные плюсы. Например, если мы будем рефакторить функцию, которая была написана в старом стиле, при помощи декоратора `asyncio.coroutine` и вызова `yield from`, и предположим, мы закомментируем или сотрем вызов конструкции `yield from`. В таком случае эта функция перестанет быть генератором, и у нас код перестанет работать ожидаемым образом. Объявление функции через конструкцию `async def` гарантирует нам, что эта функция является точно корутиной.\n", "\n", "Если мы используем этот синтаксис, то внутри мы не можем использовать конструкцию `yield from`, мы обязаны использовать вызов `await`. Давайте рассмотрим более сложный пример и напишем свой TCP сервер, который обрабатывает несколько входящих соединений одновременно." ] }, { "cell_type": "code", "execution_count": null, "id": "1fd74bfb", "metadata": {}, "outputs": [], "source": [ "# asyncio, tcp сервер\n", "import asyncio\n", "\n", "async def handle_echo(reader, writer):\n", " data = await reader.read(1024)\n", " message = data.decode()\n", " addr = writer.get_extra_info(\"peername\")\n", " print(f\"received {message} from {addr}\")\n", " writer.close()\n", " \n", "loop = asyncio.get_event_loop()\n", "coro = asyncio.start_server(handle_echo, \"127.0.0.1\", 10001, loop=loop)\n", "server = loop.run_until_complete(coro)\n", "\n", "try:\n", " loop.run_forever()\n", " \n", "except KeyboardInterrupt:\n", " pass\n", "\n", "server.close()\n", "\n", "loop.run_until_complete(server.wait_closed())\n", "loop.close()" ] }, { "cell_type": "markdown", "id": "4e816066", "metadata": {}, "source": [ "Мы уже описали подобный сервер, когда изучали процессы и потоки, и здесь все то же самое. Итак, мы получаем наш `event_loop`, делаем вызов `start_server`, передаем в этот вызов нашу корутину. В функции `start_server` мы должны еще передать параметры в виде хоста и порта, на котором мы будем слушать наше новое соединение. Далее мы запускаем установку этого соединения и делаем вызов `loop.run_forever`. Тем самым мы будем обрабатывать все входящие соединения, и после того, как мы заакцептили соединение, это все уже будет реализовано внутри кода `asyncio`, для каждого соединения будет создана отдельная корутина, и в этой корутине будет выполнена наша функция.\n", "\n", "Обратите внимание, какие удобные объекты для того, чтобы работать с нашим сокетом. Есть `reader`. При помощи конструкции `await reader`. Мы можем читать данные из нашего сокета, и также существует `writer`, если нам будет необходимо, мы сможем записывать данные в наш сокет. Давайте посмотрим, как этот пример работает в консоли. Так, нам понадобится код нашего сервера. Запускаем его при помощи команды python3." ] }, { "cell_type": "code", "execution_count": 1, "id": "d8e18ce1", "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "^C\r\n" ] } ], "source": [ "! python ex6.py" ] }, { "cell_type": "markdown", "id": "2174eced", "metadata": {}, "source": [ "Все, наш сервер готов для того, чтобы клиенты к нему подключились и отправили какую-либо информацию. Давайте создадим клиента. Пока мы создадим привычного нам синхронного клиента при помощи модуля `socket`. Запускаем еще раз интерпретатор, импортируем модуль `socket`, создаем соединение при помощи вызова `socket.create_connection`. В `create_connection` мы должны указать адресную пару. Передаем 127.0.0.1 и порт 10001. Итак, наш клиент готов. Давайте сразу подключим второй клиент в дополнительной консоли. Снова запускаем интерпретатор, делаем все то же самое. Итак, у нас сейчас готов к выполнению второй клиент. Давайте попробуем отправить данные в сокет. Помним, что в сокет нужно отправлять байтовые строки. Давайте отправим строку `ping2`. \n", "\n", "Итак, успешно отправилось 5 байт, давайте взглянем на наш сервер. Наш сервер обработал запрос со второй консоли. Давайте попробуем отправить данные с первого клиента. Пусть это будет строка `ping1`. Отправили 5 байт, посмотрели на сервер, да, действительно сервер обработал запрос и от первого клиента." ] }, { "cell_type": "code", "execution_count": 2, "id": "073cf750", "metadata": {}, "outputs": [ { "data": { "text/plain": [ "5" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import socket\n", "\n", "sock = socket.create_connection((\"127.0.0.1\", 10001))\n", "sock.send(b\"ping2\")" ] }, { "cell_type": "code", "execution_count": 3, "id": "9d84c941", "metadata": {}, "outputs": [], "source": [ "sock.close()" ] }, { "cell_type": "markdown", "id": "df6c2a09", "metadata": {}, "source": [ "Хочу обратить ваше внимание, насколько код получился простым. Во-первых, у нас нет создания процессов или потоков. Нам не нужно организовывать взаимодействие между этими процессами и потоками. Наш код работает в одном потоке. Он не захватывает `GIL`, нет проблемы с `GIL`. Код линейный, нет никаких `callback`-ов, все очень просто и понятно. Внутри этого кода заложена работа генераторов или корутин. То есть, когда приходит новый запрос, создается новый корутин и эти корутины исполняются последовательно, но, тем самым, мы смогли в одном потоке обработать несколько клиентов.\n", "\n", "Давайте рассмотрим код клиента, который тоже может быть асинхронным." ] }, { "cell_type": "code", "execution_count": null, "id": "00377e0a", "metadata": {}, "outputs": [], "source": [ "# asyncio, tcp клиент\n", "import asyncio\n", "\n", "async def tcp_echo_client(message, loop):\n", " reader, writer = await asyncio.open_connection(\"127.0.0.1\", 10001, loop=loop)\n", " print(\"send: %r\" % message)\n", " writer.write(message.encode())\n", " writer.close()\n", " \n", "loop = asyncio.get_event_loop()\n", "message = \"hello World!\"\n", "loop.run_until_complete(tcp_echo_client(message, loop))\n", "loop.close()" ] }, { "cell_type": "markdown", "id": "830ad749", "metadata": {}, "source": [ "Делается это тоже достаточно просто, мы получаем наш `event loop`. Допустим, мы будем передавать какую-то строчку. Пусть это будет всем известный \"hello world\". Мы попросим в нашем `event loop` запустить нашу функцию, которая является корутиной `tcp_echo_client`, передадим туда параметры `message` и наш `event loop`. Далее, для того, чтобы создать соединение, мы должны вызвать метод `asyncio.open_connection`. Точно в этот вызов мы должны отправить адресную пару, куда мы делаем соединение, и вызов `await` вернет нам `reader` и `writer`. Это два объекта, при помощи которых можно взаимодействовать с нашим удаленным сервером. То есть, при помощи объекта `reader` можно читать данные с сервера, при помощи объекта `writer` можно записывать данные на наш сервер. Опять же, вы можете легко создать несколько таких асинхронных клиентов и одновременно выполнять запросы на разные сервера, при этом не делая никаких потоков или процессов. Это очень удобно, просто, и код получается достаточно производительным.\n", "\n", "Итак, на этой лекции мы сделали первые шаги для работы с библиотекой `asyncio`. Мы разобрали несколько простых примеров, остановили внимание на том, как они работают, а также разобрали пример работы сетевой программы с использованием библиотеки `asyncio`. На следующей лекции мы продолжим изучать то, как устроено `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 }