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.

206 lines
19 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": "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
}