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