You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

311 lines
20 KiB
Plaintext

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

{
"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 <module>\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
}