{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "d3abcf81",
   "metadata": {},
   "source": [
    "# Исполнение кода в одном потоке, модуль select #"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f549add",
   "metadata": {},
   "source": [
    "Итак, на этой лекции мы поговорим об исполнении кода в один поток. В предыдущих лекциях мы рассмотрели использование процессов и потоков для организации взаимодействия большого количества клиентов по сети сервера. Исполнение кода в один поток — это немного другой подход, и сегодня мы остановим внимание на использовании модуля `select`, поговорим про неблокирующий ввод-вывод в Python, а также обсудим некоторые популярные фреймворки в Python.\n",
    "\n",
    "В операционной системе существует модуль `select`, который позволяет организовать работу с неблокирующим вводом-выводом. Что это значит, мы сейчас будем разбирать на примерах.\n",
    "\n",
    "Отдельно хочется сказать, что существует несколько подходов или механизмов опроса всех файловых дескрипторов для организации неблокирующего ввода-вывода. Эти методы перечислены на слайде: это `select.select`, `select.poll`, `select.epoll` и `select.kqueue`. Все эти методы связаны с особенностями операционных систем. Например, в Linux, как правило, используют `epoll`, в других операционных системах могут использоваться другие механизмы. Мы будем рассматривать примеры на основе `epoll`.\n",
    "\n",
    "Итак, давайте вспомним наш предыдущий пример и попробуем реализовать его немного по-другому. Вспомним, что у нас была проблема создания сокета и обработки нескольких входящих соединений одновременно. Давайте попробуем сделать это при помощи модуля `select`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b3bd1dd7",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Неблокирующий ввод/вывод, обучающий пример\n",
    "import socket\n",
    "import select\n",
    "\n",
    "sock = socket.socket()\n",
    "sock.bind((\"\", 10001))\n",
    "sock.listen()\n",
    "\n",
    "# как обработать запросы для conn1 и conn2\n",
    "# одновременно без потоков?\n",
    "conn1, addr = sock.accept()\n",
    "conn2, addr = sock.accept()\n",
    "\n",
    "conn1.setblocking(0)\n",
    "conn2.setblocking(0)\n",
    "\n",
    "epoll = select.epoll()\n",
    "epoll.register(conn1.fileno(), select.EPOLLIN | select.EPOLLOUT)\n",
    "epoll.register(conn2.fileno(), select.EPOLLIN | select.EPOLLOUT)\n",
    "\n",
    "conn_map = {\n",
    "    conn1.fileno(): conn1,\n",
    "    conn2.fileno(): conn2,\n",
    "}"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f492bfd6",
   "metadata": {},
   "source": [
    "Итак, предположим, мы создали объект класс `socket`, вызвали метод `bind`, вызвали метод `listen` и смогли вызвать метод `accept` для двух соединений. Теперь у нас есть два объекта соединения — `connection1` и `connection2`, и нам необходимо одновременно читать или записывать данные из этих соединений без использования потоков или процессов. Как можно это организовать в Linux? Для этого необходимо перевести наше соединение, во-первых, в неблокирующий режим. Делается это при помощи вызова `setblocking(0)`. Это равносильно тому, что мы сделаем вызов `settimeout(0)`. Теперь наши сокеты в неблокирующем режиме, и если мы попробуем что-то прочитать из этого сокета, а там данных нет, то наш вызов `recv` не заблокируется, а вернет нам некую системную ошибку, которая будет говорить о том, что пока данных нет, они еще не поступили. Но как узнать, какие сокеты готовы читать, а какие сокеты готовы записывать данные? Для этого как раз нам понадобится объект `epoll`. Итак, создаем объект `epoll` при помощи модуля `select`. Далее мы регистрируем в этом объекте `epoll` наши файловые дескрипторы от созданных коннектов клиентских, а также мы регистрируем некую маску. Говорим, на какие события мы подписываемся от этих файловых дескрипторов. В данном случае это чтение из сокетов и запись в сокет. Далее для организации цикла опроса событий нам необходимо запомнить наши объекты и смапить их по файловым дескрипторам. То есть формируем словарик, в него записываем файловые дескрипторы и объекты наших соединений. Давайте рассмотрим цикл обработки событий для нашего `epoll`, еще иногда его называют `event_loop`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "18ada3b9",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Неблокирующий ввод/вывод, обучающий пример\n",
    "# Цикл обработки событий в epoll\n",
    "\n",
    "while True:\n",
    "    events = epoll.poll(1)\n",
    "    \n",
    "    for fileno, event in events:\n",
    "        if event & select.EPOLLIN:\n",
    "            # обработка чтения из сокета\n",
    "            data=conn_map[fileno].recv(1024)\n",
    "            print(data.decode(\"utf8\"))\n",
    "            \n",
    "        elif event & select.EPOLLOUT:\n",
    "            # обработка записи в сокет\n",
    "            conn_map[fileno].send(\"pong\".encode(\"utf8\"))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "30878ebd",
   "metadata": {},
   "source": [
    "Итак, мы в бесконечном цикле постоянно опрашиваем наш объект `epoll`. Это системный вызов, который нам возвращает список событий, и эти события содержат файловый дескриптор и непосредственно то событие, которое произошло с этим файловым дескриптором. Другими словами, мы постоянно просим наш объект `epoll`, говорим ему: верни нам список сокетов, которые готовы читать либо которые готовы записывать данные. После того как нам `epoll` вернул этот список, мы должны проверить, что за событие пришло, из нашего словаря получить нужный нам объект для работы с ним и сделать нужные нам действия. В данном случае, например, мы читаем данные из этого сокета и просто выводим их в консоль. если пришло событие, которое говорит нам, что сокет готов принять данные от нас, мы должны записать данные в этот сокет. Давайте попробуем запустить этот пример и посмотрим, как он действительно работает в консоли.\n",
    "\n",
    "Итак, нам потребуется код нашего сервера, запускаем его при помощи команды python3, и для того чтобы работал, необходимо создать два клиентских подключения."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "82247ea9",
   "metadata": {},
   "outputs": [],
   "source": [
    "! python ex1.py"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "eafad08e",
   "metadata": {},
   "source": [
    "Давайте попробуем это сделать прямо в консоли, создаем объект класса `socket`, передаем адресную пару, порт. Итак, один клиент у нас уже готов для взаимодействия с нашим сервером. Давайте создадим еще один клиент. Точно так же мы создали второй клиент Теперь мы можем записать данные непосредственно в `socket`. Итак, записываем данные во втором клиенте, например, строчку `client2`, отправляем, смотрим — наш сервер обработал второе соединение. Затем можем сделать то же самое и в первом клиенте."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "def7b30c",
   "metadata": {},
   "outputs": [],
   "source": [
    "import socket\n",
    "\n",
    "sock = socket.create_connection((\"127.0.0.1\", 10001))\n",
    "sock.sendall(b\"client1\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "d6563052",
   "metadata": {},
   "outputs": [],
   "source": [
    "sock2 = socket.create_connection((\"127.0.0.1\", 10001))\n",
    "sock2.sendall(b\"client2\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "c2f19409",
   "metadata": {},
   "outputs": [],
   "source": [
    "sock2.close()\n",
    "sock.close()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "222607a8",
   "metadata": {},
   "source": [
    "Как мы видим, сервер успешно обработал оба соединения, причем он не использовал ни процессы, ни потоки.\n",
    "\n",
    "Основная идея заключается в том, что у нас есть `event_loop`, есть `epoll` который позволяет нам получить все нужные события, и при помощи `epoll` можно организовать параллельную обработку всех запросов.\n",
    "\n",
    "Итак, давайте обсудим плюсы и минусы того подхода, который мы сейчас обсудили и запустили в консоли. Итак, код выглядит уже не таким простым, как в случае с потоками или процессами. Появился цикл событий, появились вот эти условия для обработки типов событий, которые там произошли. Наш код носит обучающий характер, и он достаточно простой, то есть если он будет приближен к настоящему коду, который работает с сетевыми соединениями, он очень сильно усложнится. Например, в нашем коде нет обработки закрытия сокетов, а также отсутствует обработка новых входящих соединений. Заметьте, мы ведь точно так же должны перевести и сокет, на котором ждем входящее соединение, в неблокирующий режим и точно так же поместить его в наш цикл обработки событий для того, чтобы ожидать новых клиентов.\n",
    "\n",
    "Давайте предположим, что в обработке наших запросов будет не только вывод данных, которые мы получили в сети, в `log` или в терминал, а нам нужно будет, например, записать их в какую-то базу данных. Или, например, выполнить http-запрос. В таком случае нам нужно будет организовать ввод-вывод и всю работу с ним точно так же в неблокирующем режиме. То есть нам надо все сокеты из базы данных поместить в наш `event_loop`, или в наш цикл событий, и точно так же работать с ними в неблокирующем режиме. Тогда код станет уже не таким простым. Точно так же любые сторонние библиотеки, если мы будем использовать, например, `urllib` или библиотеку `requests`, мы тоже будем вынуждены как-то заставить их работать с нашим циклом событий и использовать `epoll` для ожидания готовности чтения и записи в сокеты, которые они используют.\n",
    "\n",
    "Но тем не менее, очевидным плюсом данного подхода является то, что мы, во-первых, не создаем ни процессы, ни потоки, мы не тратим память дополнительную, а также, что немаловажно, мы не тратим никаких усилий для организации взаимодействия между нашими потоками: у нас нет блокировок, код будет исполняться максимально быстро.\n",
    "\n",
    "Также у нас нет проблем с `GIL`. Как же можно поступить, чтобы упростить наш код? Можно спрятать все вызовы `select.epoll` в какие-то библиотеки. Так поступили и написали существующие в Python фреймворки. Давайте обсудим некоторые из них.\n",
    "\n",
    "Наиболее популярным был долгое время фреймворк `Twisted`, его работа очень похожа на наш пример, но код на `Twisted` выглядит достаточно сложным из-за наличия вот этих `callback`'ов и логики обработки запросов.\n",
    "\n",
    "Чуть позже появился фреймворк под названием `Gevent`. `Gevent` напоминал клон Python'а, который назывался `Stackless Python`, и в основу фреймворка `Gevent` положили работу гринлетов — это так называемые легковесные потоки. Они очень похожи на наши потоки, которые мы рассматривали на предыдущей лекции, но внутри они устроены более легковесно, и переключение между ними не требует почти никаких ресурсов операционной системы.\n",
    "\n",
    "Также один из известных фреймворков — это `Tornado`. Он был основан на `api` генераторов Python и устроен немного иначе, чем фреймворк `Gevent`.\n",
    "\n",
    "После Tornado в Python3 появился фреймворк `AsyncIO`, и он сейчас является мейнстримом. В его основе, в принципе, лежит то же самое, что и в `Tornado`, но тем не менее `AsyncIO` поставляется вместе с Python3 Core, он также основан на работе генераторов. Его поддерживают официальные сообщества, и поэтому все наши дальнейшие разговоры о программировании в один поток будут привязаны к фреймворку `AsyncIO`.\n",
    "\n",
    "Итак, мы обсудили, как работает неблокирующий ввод-вывод, иначе его еще называют мультиплексированием. Такой подход иначе называют мультиплексированием ввода-вывода.\n",
    "\n",
    "Мы рассмотрели пример простой программы, как использовать объект `select.epoll`, а также немного обсудили популярные фреймворки. И в дальнейших лекциях мы продолжим изучать работу `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
}
