О том что такое асинхронное программирование написано уже не мало, а вот о практических преимуществах как то не много. Ниже я покажу наглядный пример о том что это такое, как работает и почему это хорошо.
Для эталонного примера возьмем типовой рекурсивный алгоритм нахождения факториала:
И напишем используя aiohttp асинхронное приложение для расчета факториала. При этом каждый запрос будет обрабатывать одну итерацию рекурсии. Как это? Будет всего одна функция-обработчик запроса, где входное значение будет передаваться через GET параметр, а результат будет возвращаться в виде JSON.
Попробуем рассчитать факториал числа 10, для этого сделай простой запрос
Итак, что здесь примечательного?
Порядок ответа сервера. От значения 1 до 10, это порядок обратный порядку запросов. Действительно, первым запросом пришедшим на сервер был запрос на /factorial?value=10, но полностью обработан он был последним. Именно так работает алгоритм, который был показан в само начале.
Время ответа сервера. Время ответа от запроса к запросу примерно одинаковое,а в общем изменяется линейно. Самым долгим был обработан запрос на значение 10, а быстрым на значение 1. Это великолепно показывает "вложенность" обработки запросов друг в друга.
Источик запроса. Самым первым принятым и последним обработанным был запрос сделанный руками с помощью curl, а все остальные самим сервером с помощью клиента aiohttp библиотеки.
Но главное здесь другое - один процесс сервера смог принять и обработать одновременно десяток запросов. Действительно, на момент обработки последнего запроса все остальные находились в ожидании ответа сервером.
Ну а что же в случае с синхронным сервером?
Простой код ниже реализует тот же алгоритм.
Но при попытке аналогичного запроса в логах будет только скромное
В отличие от асинхронного подхода, классический синхронный однопоточный сервер на момент обработки запроса блокируется для выполнения другого.
Для эталонного примера возьмем типовой рекурсивный алгоритм нахождения факториала:
def factorial(n): if n == 1: return 1 else: return n * factorial(n - 1) assert factorial(1) == 1 assert factorial(5) == 120 assert factorial(10) == 3628800
И напишем используя aiohttp асинхронное приложение для расчета факториала. При этом каждый запрос будет обрабатывать одну итерацию рекурсии. Как это? Будет всего одна функция-обработчик запроса, где входное значение будет передаваться через GET параметр, а результат будет возвращаться в виде JSON.
from logging import getLogger, StreamHandler, DEBUG import json import aiohttp from aiohttp import web async def handler_factoial(request): value = int(request.query['value']) if value == 1: return web.json_response({'value': 1}) url = '{}://{}{}'.format(request.scheme, request.host, request.path) async with aiohttp.ClientSession() as session: resp = await session.get(url, params={'value': value - 1}) data = json.loads(await resp.text()) return web.json_response({'value': value * int(data['value'])}) if __name__ == '__main__': app = web.Application() app.add_routes([web.get('/factorial', handler_factoial)]) log = getLogger('aiohttp.access') log.setLevel(DEBUG) log.addHandler(StreamHandler()) log_format = '%a %t "%r" %s %b "%{User-Agent}i" %Tfsec' web.run_app(app, port=8000, access_log=log, access_log_format=log_format)
Попробуем рассчитать факториал числа 10, для этого сделай простой запрос
$ curl http://localhost:8000/factorial?value=10 {"value": 3628800}Но самое интересное можно наблюдать в логе приложения (вывод слегка обрезан для наглядности)
"GET /factorial?value=1 HTTP/1.1" 200 169 "Python/3.6 aiohttp/3.3.2" 0.001309sec "GET /factorial?value=2 HTTP/1.1" 200 169 "Python/3.6 aiohttp/3.3.2" 0.005224sec "GET /factorial?value=3 HTTP/1.1" 200 169 "Python/3.6 aiohttp/3.3.2" 0.009071sec "GET /factorial?value=4 HTTP/1.1" 200 170 "Python/3.6 aiohttp/3.3.2" 0.012926sec "GET /factorial?value=5 HTTP/1.1" 200 171 "Python/3.6 aiohttp/3.3.2" 0.016710sec "GET /factorial?value=6 HTTP/1.1" 200 171 "Python/3.6 aiohttp/3.3.2" 0.021433sec "GET /factorial?value=7 HTTP/1.1" 200 172 "Python/3.6 aiohttp/3.3.2" 0.025436sec "GET /factorial?value=8 HTTP/1.1" 200 173 "Python/3.6 aiohttp/3.3.2" 0.029291sec "GET /factorial?value=9 HTTP/1.1" 200 174 "Python/3.6 aiohttp/3.3.2" 0.033610sec "GET /factorial?value=10 HTTP/1.1" 200 175 "curl/7.58.0" 0.054997sec
Итак, что здесь примечательного?
Порядок ответа сервера. От значения 1 до 10, это порядок обратный порядку запросов. Действительно, первым запросом пришедшим на сервер был запрос на /factorial?value=10, но полностью обработан он был последним. Именно так работает алгоритм, который был показан в само начале.
Время ответа сервера. Время ответа от запроса к запросу примерно одинаковое,а в общем изменяется линейно. Самым долгим был обработан запрос на значение 10, а быстрым на значение 1. Это великолепно показывает "вложенность" обработки запросов друг в друга.
Источик запроса. Самым первым принятым и последним обработанным был запрос сделанный руками с помощью curl, а все остальные самим сервером с помощью клиента aiohttp библиотеки.
Но главное здесь другое - один процесс сервера смог принять и обработать одновременно десяток запросов. Действительно, на момент обработки последнего запроса все остальные находились в ожидании ответа сервером.
Ну а что же в случае с синхронным сервером?
Простой код ниже реализует тот же алгоритм.
import json from http.server import SimpleHTTPRequestHandler, HTTPServer import requests class HttpProcessor(SimpleHTTPRequestHandler): def get_value(self): s = self.path.partition('?')[-1] k, v = s.split('=') if k == 'value': return int(v) else: raise RuntimeError() def do_GET(self): value = self.get_value() print('Get /?value={}'.format(value)) if value == 1: return self.return_json_response({'value': 1}) resp = requests.get('http://localhost:8000/?value={}'.format(value - 1)) nvalue = resp.json()['value'] return self.return_json_object({'value': value * nvalue}) def return_json_response(self, data): self.send_response(200) self.send_header('content-type', 'application/json') self.end_headers() s_data = json.dumps(data) self.wfile.write(s_data.encode('u8')) if __name__ == '__main__': serv = HTTPServer(("localhost", 8000), HttpProcessor) serv.serve_forever()
Но при попытке аналогичного запроса в логах будет только скромное
Get /?value=10и долгое ожидание
В отличие от асинхронного подхода, классический синхронный однопоточный сервер на момент обработки запроса блокируется для выполнения другого.
> один процесс сервера смог принять и обработать одновременно десяток запросов
ОтветитьУдалитьКак же одновременно, если они выполнились все по порядку?
> Действительно, первым запросом пришедшим на сервер был запрос на /factorial?value=10, но полностью обработан он был последним
Так в итоге в этом конкретном примере время обработки синхроном режиме во сколько раз больше?
> Как же одновременно, если они выполнились все по порядку?
ОтветитьУдалитьв этом примере они выполняются последовательно, так как один запрос порождает другой. Но в целом, это работает примерно так. Пришел тяжелый запрос и в процессе выполнения приложение ждет ответа от бд, то есть простаивает. В этотм момент если приходит второй запрос, то приожение способно переключиться и выполнить его. Причем вполе нормальная ситуация что ответ на товрой запрос был отдан раньше чем вернулось управление к первому запросу. В случае с синхронным приложением обработка первого запроса полностью блокирует выполнение потока и если пришел второй запрос, то он начнет выполняться только после завершения первого.
> Так в итоге в этом конкретном примере время обработки синхроном режиме во сколько раз больше?
Вы совсем не поняли видимо последний абзац. Конкретно в этом примере синхронный код не выполнится. Никогда. Он повиснет в бесконечность. Потмоу что произойдет блокирование. Сервер заблокирует сам себя. В случае если используется горизонтальное расширение, например приложение запускатеся через gunicorn с 4 воркерами, то это означает что можно будет посчитать факториал 4, но не 5 так как закончатся свободный воркреры и произойдет снова блокировка. Такой тип блокировок еще называли deadlock. Потмоу что они никогда не разрешатся.
ОтветитьУдалитьне могу понять, почему этот скрипт вываливает результаты только в конце исполнения, и почему после каждого GET запроса есть ожидание в течение 1ой секунды?
python factorial.py
======== Running on http://0.0.0.0:8000 ========
(Press CTRL+C to quit)
127.0.0.1 [25/Dec/2019:15:19:21 +0000] "GET /factorial?value=1 HTTP/1.1" 200 169 "Python/3.7 aiohttp/3.6.2" 0.000000sec
127.0.0.1 [25/Dec/2019:15:19:20 +0000] "GET /factorial?value=2 HTTP/1.1" 200 169 "Python/3.7 aiohttp/3.6.2" 1.016000sec
127.0.0.1 [25/Dec/2019:15:19:19 +0000] "GET /factorial?value=3 HTTP/1.1" 200 169 "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.108 Safari/537.36" 2.063000sec
и не очень понятно где именно происходит порождение нового запроса для всех value>1
ОтветитьУдалить