Skip to content

Latest commit

 

History

History
902 lines (755 loc) · 38.3 KB

README.md

File metadata and controls

902 lines (755 loc) · 38.3 KB

Асинхронность

История асинхронной разработки на python ^

Год События
2002 появление Twisted, event loop - "Reactor". Позже появились свои Future-объекты, называемые Deferred, корутины @inlineCallbacks, возможность выполнить задачу в пуле потоков deferToThread(func)
2009 появление библиотеки gevent. Она позволяла оборачивать задачи в "зеленые потоки" (интерфейс похож на модуль threading, однако не использует потоки ОС) и выполнять их на event loop (на libev или libuv). При переключении гринтредов заменялся системный стек. Недостатки: приходилось патчить методы (monkey.patch_all()) и yield не поддерживался
появление Tornado, которую мы используем в настоящее время
2012 Гвидо ван Россум написал первую, пробную, версию asyncio, пока в качестве отдельного пакета под python 3.3. При дальнейшей разработке core-разработчики python консультировались с создателями Twisted и Tornado и переняли все хорошее, что там было
2013 появление в Tornado 3 корутин @gen.coroutine, используемых с yield и базирующихся на генераторах, для более удобного (по сравнению с коллбечным) написания кода *
Андрей Светлов выпустил первую версию библиотеки aiohttp - реализацию http-протокола для asyncio. В стандартную библиотеку python aiohttp решили не добавлять
2014 asyncio стала частью стандартной библиотеки python 3.4, с корутинами на декораторах @asyncio.coroutine, используемых с yield from *
в Tornado 3 появилась экспериментальная поддержка asyncio
2015 в python 3.5 появился новый синтакис для корутин - async и await *, а также инструкции async with и async for. Такие корутины работают быстрее, чем старые, с декоратором. Также добавили метод loop.create_task()
2016 появление поддержки event loop из asyncio в Twisted
в python 3.6 появилась поддержка асинхронных генераторов (можно использовать yield в корутинах), async comprehensions
появление ультра-быстрого веб-фреймворка sanic, который является реализацией http-протокола для asyncio
2018 в Tornado 5 стали использоваться Futures, Tasks и event loop из asyncio вместо своих, поддерживаются теперь нативные корутины async def
в python 3.7 методы из asyncio сделали более удобными для использования (например, добавили asyncio.run()) и улучшили производительность, добавили contextvars - аналог thread-local переменных для асинхронного кода
2019 в Tornado 6 улучшена поддержка asyncio, нативных корутин, выпилены коллбеки из большинства методов

Библиотеки Twisted, Gevent и Tornado, хоть и не совсем заброшены (версии новые выходят), но в новых проектах уже не используются, так как все необходимое, кроме разве что реализации http-протокола, уже есть в стандартной библиотеке в модуле asyncio. Современные Twisted и Tornado также имеют поддержку asyncio.

Сравнение корутин tornado, старых корутин asyncio и нативных корутин

Корутины tornado Старые корутины asyncio Нативные корутины
Базируются на генераторах. Используются совместно с `yield`. После добавления в tornado 5 поддержки нативных корутин их использование не рекомендуется. [Документация](https://www.tornadoweb.org/en/stable/guide/coroutines.html)
from tornado import gen

@gen.coroutine
def a():
    b = yield c()
    return b
Появились в версии 3.4. Базируются на генераторах. Используются совместно с `yield from` (в отличие от корутин tornado). [Документация](https://docs.python.org/3.4/library/asyncio-task.html#coroutines). После появления в версии 3.5 нативных корутин их использование не рекомендуется, и в версии 3.10 они будут выпилены.
import asyncio

@asyncio.coroutine
def a():
    b = yield from c()
    return b
Появились в версии 3.5. Подвид генераторов. Используются совместно с `await`. Часть языка, а не какой-либо библиотеки. Хорошо оптимизированы и рекомендуются к использованию.
async def a():
    b = await c()
    return b

Приложения на корутинах tornado, а также приложения на старых корутинах asyncio работают медленнее, чем приложения на нативных корутинах. Рекомендуеся использовать исключительно последние.

Асинхронность на уровне ОС* ^

Все перечисленные выше библиотеки, Asyncio, Twisted, Gevent и Tornado, основаны на одном механизме - неблокирующем вводе-выводе и мультиплексировании, как и множество подобных проектов, например, nodejs, браузеры, nginx, qt, gtk, netty (EpollEventLoop.java), tokio (rust), evio (go), amp (php), eventmachine (ruby), coro-async(C++).

* Здесь и далее мы не рассматриваем другой тип асинхронности, основанный только на пуле потоков и применяемый в C#, kotlin, java (CompletableFuture). У нас же пул потоков - лишь вспомогательное решение для ряда операций (не pollable).

Все подобные приложения используют под капотом примерно такой код (на linux, для mac же используется kqueue):

int ep = epoll_create(1);
struct epoll_event new_ev;
new_ev.data.fd = server;
new_ev.events = EPOLLIN;
epoll_ctl(ep, EPOLL_CTL_ADD, server, &new_ev);
while(1) {
    if (epoll_wait(ep, &new_ev, 1, 2000) == 0) {
        printf("Timeout\n");
            continue;
    }
    if (new_ev.data.fd == server) {
        int client_sock = accept(server, NULL, NULL);
        printf("New client\n");
        new_ev.data.fd = client_sock;
        new_ev.events = EPOLLIN;
        epoll_ctl(ep, EPOLL_CTL_ADD, client_sock, &new_ev);
    } else {
        printf("Interact with fd %d\n", (int)new_ev.data.fd);
        if (interact(new_ev.data.fd) == 0) {
            printf("Client disconnected\n");
            close(new_ev.data.fd);
            epoll_ctl(ep, EPOLL_CTL_DEL, new_ev.data.fd, NULL);
        } 
    }
} 
close(ep);

Разберем подробнее присутствующие в этом коде системные вызовы:

  • int epoll_create(int size), int epoll_create1(int flags) - создает экземпляр epoll и возвращает указывающий на него файловый дескриптор
  • int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); - добавляет, изменяет, удаляет дескрипторы из списка интереса экземпляра epoll. В объекте event содержится тип нужного события и пользовательские данные
  • int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout) - ожидает события на экземпляре epoll, возвращает произошедшие отслеживаемые события
  • close(int fd) - закрывает дескриптор

Последовательность операций:

  1. Создаем экземпляр epoll
  2. Делаем все дескрипторы, которые нужно отслеживать, неблокирующими
  3. Формируем список интереса epoll с помощью вызова epoll_ctl(), добавляя и удаляя из него эти дескрипторы
  4. В цикле обрабатываем события ввода/вывода:
    • извлекаем список готовых дескрипторов, используя epoll_wait()
    • выполняем ввод/вывод для каждого готового дескриптора, пока соответствующий системный вызов (например, readQ, writeQ, recvQ, sendQ или acceptQ) не вернет ошибку EAGAIN или EWOULDBLOCK

Pollable-операции:

  • net, dgram, http, tls, https
  • child process pipes
  • stdin, stdout, stderror
  • таймауты epoll_wait(..., int timeout)
  • сигналы

Не pollable-операции:

  • все, что касается работы с файловой системой
  • работа с dns (а именно dns.lookup(), вызывающий блокирующую getaddrinfo(), остальные методы dns.* - не блокирующие) Такие операции выполняются вне event loop'а, на пуле потоков, и сообщают ему, когда работа будет завершена (используя eventfd или self-pipe - создается в потоке, слушается лупом)

epoll_wait - блокирующий вызов (до того момента, когда какие-либо дескрипторы из списка наблюдаемых, будут готовы на чтение или на запись). Посмотрим на реальном примере: запустим пример асинхронного веб-сервера async_server_example.py на sanic через strace, а затем сделаем к нему запрос с другой консоли: curl localhost:8080.

vera@vera:~$ strace -e epoll_create1,epoll_wait,epoll_ctl python3 async_server_example.py  >> /dev/null
--- SIGCHLD {si_signo=SIGCHLD, si_code=CLD_EXITED, si_pid=7144, si_uid=2000, si_status=0, si_utime=0, si_stime=0} ---
epoll_create1(EPOLL_CLOEXEC)            = 3
epoll_ctl(3, EPOLL_CTL_ADD, 9, {EPOLLIN, {u32=4294967295, u64=4294967295}}) = 0
epoll_ctl(3, EPOLL_CTL_DEL, 9, 0x7ffcb2e0e3cc) = 0
epoll_ctl(3, EPOLL_CTL_DEL, 9, 0x7ffcb2e0e3ec) = -1 ENOENT (No such file or directory)
epoll_ctl(3, EPOLL_CTL_ADD, 6, {EPOLLIN, {u32=6, u64=6}}) = 0
epoll_ctl(3, EPOLL_CTL_ADD, 8, {EPOLLIN, {u32=8, u64=8}}) = 0
epoll_ctl(3, EPOLL_CTL_ADD, 9, {EPOLLIN, {u32=9, u64=9}}) = 0
epoll_wait(3, [{EPOLLIN, {u32=8, u64=8}}], 1024, 0) = 1
epoll_wait(3, [{EPOLLIN, {u32=8, u64=8}}], 1024, 0) = 1
epoll_wait(3, [], 1024, 0)              = 0
epoll_ctl(3, EPOLL_CTL_ADD, 11, {EPOLLIN, {u32=11, u64=11}}) = 0
epoll_wait(3, [], 1024, 0)              = 0
epoll_wait(3, [{EPOLLIN, {u32=11, u64=11}}], 1024, -1) = 1
epoll_ctl(3, EPOLL_CTL_ADD, 13, {EPOLLIN, {u32=13, u64=13}}) = 0
epoll_wait(3, [{EPOLLIN, {u32=13, u64=13}}], 1024, 60000) = 1
epoll_wait(3, [], 1024, 0)              = 0
epoll_wait(3, [{EPOLLIN, {u32=13, u64=13}}], 1024, 4999) = 1
epoll_ctl(3, EPOLL_CTL_DEL, 13, 0x7ffcb2e0debc) = 0
epoll_wait(3, [], 1024, 0)              = 0
epoll_wait(3, # ... ждем новые события

Библиотека asyncio ^

Асинхронная разработка в питоне базируется на event loop'е и awaitable-объектах.

Для удобства библиотека asyncio была разделена на высокоуровненое (пользовательский код) и низкоуровневое апи (код библиотек).

Awaitable-объекты ^

В питоне существует три типа awaitable-объектов (имеют метод __await__ и могут использоваться с инструкцией await): корутины, фьючи, таски.

Корутины ^

  • Есть функции-корутины и объекты-корутины, получаемые путем вызова функций-корутин
  • Фактически, на уровне интерпретатора, это подвид генераторов
  • Для выполнения на event loop'е преобразуется в Task/Future

Фьючи ^

  • Future: код base_futures.py, futures
  • Имеют три состояния: PENDING, CANCELLED, FINISHED. Важные методы: cancel, result, exception, set_result, set_exception, add_done_callback, remove_done_callback
  • Низкоуровневый awaitable-объект, представляющий результат асинхронной операции
  • Обычно не используется в пользовательском коде
  • Если мы авейтим фьючу, то дожидаемся когда у нее появится результат или исключение
  • Используется для того, чтобы коллбечный код мог использоваться с async-await
  • Некоторые функции из библиотеки asyncio, например loop.run_in_executor(), возвращают фьючи
  • Создаются вызовом loop.create_future()

Таски ^

  • Task: наследуются от Future, код base_tasks.py, tasks.py
  • Если мы авейтим корутину, она под капотом неявно преобразается в таску
  • В отличие от Future, имеют дополнительный метод __step, в котором у оборачиваемой корутины делается result = coro.send(None)
  • Вручную можно создать таску через asyncio.create_task
  • После создания таска сразу ставится на выполнение на event loop
  • Таску можно авейтить ниже в коде либо не авейтить вообще
  • Если таску не авейтить - фактически это будет выполнение задачи в фоновом режиме
  • У таски есть метод .cancel() и ее можно отменить
  • Таска наследется от Future, и у нее так же есть метод .add_done_callback
Корутины Futures Tasks
>>> import asyncio

>>> async def main():
... print('hello')
... await asyncio.sleep(1)
... print('world')
    
>>> coro = main()  
<stdin>:1: RuntimeWarning: coroutine 'main' was never awaited
Object allocated at (most recent call last):
  File "<stdin>", lineno 1
  
>>> coro
<coroutine object main at 0x104709740>
    
>>> dir(coro)
['__await__', '__class__', '__del__', '__delattr__', '__dir__', 
'__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', 
'__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', 
'__lt__', '__name__', '__ne__', '__new__', '__qualname__', 
'__reduce__', '__reduce_ex__', '__repr__', '__setattr__', 
'__sizeof__', '__str__', '__subclasshook__', 'close', 'cr_await', 
'cr_code', 'cr_frame', 'cr_origin', 'cr_running', 'send', 'throw']

>>> asyncio.run(main())
hello
world
>>> import asyncio

>>> async def set_after(fut, delay, value):
...    await asyncio.sleep(delay)
...    fut.set_result(value)

>>> fut = None

>>> async def main():
...    loop = asyncio.get_running_loop()
...    global fut
...    fut = loop.create_future()
...    loop.create_task(set_after(fut, 1, '... world'))
...    print('hello ...')
...    print(await fut)

>>> asyncio.run(main())

>>> fut
<Future finished result='... world' created 
at ....python3.9/asyncio/base_events.py:424>

>>> dir(fut)
['__await__', '__class__', '__class_getitem__', '__del__', 
'__delattr__', '__dir__', '__doc__', '__eq__', '__format__', 
'__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', 
'__init_subclass__', '__iter__', '__le__', '__lt__', '__ne__', 
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', 
'__sizeof__', '__str__', '__subclasshook__', '_asyncio_future_blocking',
 '_callbacks', '_cancel_message', '_exception', '_log_traceback', 
 '_loop', '_make_cancelled_error', '_repr_info', '_result', 
 '_source_traceback', '_state', 'add_done_callback', 'cancel', 
 'cancelled', 'done', 'exception', 'get_loop', 'remove_done_callback', 
 'result', 'set_exception', 'set_result']
>>> import asyncio

>>> async def nested():
...    return 42

>>> task = None

>>> async def main():
...    global task
...    task = asyncio.create_task(nested())
...    await task

>>> asyncio.run(main())

>>> task
<Task finished name='Task-6' coro=<nested() done, defined at <stdin>:1> 
result=42 created at ....python3.9/asyncio/tasks.py:361>

>>> dir(task)
['__await__', '__class__', '__class_getitem__', '__del__', 
'__delattr__', '__dir__', '__doc__', '__eq__', '__format__', 
'__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', 
'__init_subclass__', '__iter__', '__le__', '__lt__', '__ne__', 
'__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', 
'__sizeof__', '__str__', '__subclasshook__', '_asyncio_future_blocking', 
'_callbacks', '_cancel_message', '_coro', '_exception', '_fut_waiter', 
'_log_destroy_pending', '_log_traceback', '_loop', '_make_cancelled_error', 
'_must_cancel', '_repr_info', '_result', '_source_traceback', '_state', 
'add_done_callback', 'cancel', 'cancelled', 'done', 'exception', 'get_coro', 
'get_loop', 'get_name', 'get_stack', 'print_stack', 'remove_done_callback', 
'result', 'set_exception', 'set_name', 'set_result']

Корутины (async-await синтаксис) - это часть языка, а не библиотеки asyncio, что позволило написать альтернативные asyncio любопытные библиотеки: curio от Девида Бизли и trio от Натаниэля Смита.

Несколько примеров

Реализации event loop для asyncio ^

Для использования с библиотекой asyncio event loop должен реализовывать методы класса AbstractEventLoop:

  • run_forever()
  • run_until_complete()
  • stop()
  • is_running()
  • is_closed()
  • close()
  • shutdown_asyncgens()
  • shutdown_default_executor()
  • call_soon()
  • call_later()
  • call_at()
  • time()
  • create_future()
  • create_task()
  • call_soon_threadsafe()
  • run_in_executor()
  • set_default_executor()
  • getaddrinfo()
  • getnameinfo()
  • create_connection()
  • create_server()
  • sendfile()
  • start_tls()
  • create_unix_connection()
  • create_unix_server()
  • create_datagram_endpoint()
  • connect_read_pipe()
  • connect_write_pipe()
  • subprocess_shell()
  • subprocess_exec()
  • add_reader()
  • remove_reader(self, fd)
  • add_writer()
  • remove_writer()
  • sock_recv()
  • sock_recv_into()
  • sock_sendall()
  • sock_connect()
  • sock_accept()
  • sock_sendfile()
  • add_signal_handler()
  • remove_signal_handler()
  • set_task_factory()
  • get_task_factory()
  • get_exception_handler()
  • set_exception_handler()
  • default_exception_handler()
  • call_exception_handler()
  • get_debug()
  • set_debug()

Стандартный ^

Написан на питоне. Используется по умолчанию.

uvloop ^

Написан Юрием Селивановым с использованием libuv в 2016 г. В настоящее время часто используется в продакшене, даже в крупных компаниях. По сравнению со стандартным дает прирост производительности в 2-4 раза (по данным статьи), на реальных проектах результат скромнее, но тоже ощутим (согласно замерам, проведенным в компании Rambler, ~30%).

Как использовать:

import asyncio
import uvloop

asyncio.set_event_loop_policy(uvloop.EventLoopPolicy())

tokio ^

Написан с использованием tokio.rs в 2017 г., с 2018 г. проект не поддерживается более.

Как использовать:

import asyncio
import tokio

asyncio.set_event_loop_policy(tokio.EventLoopPolicy())

Генераторы и корутины ^

Вообще, корутины появились довольно давно и используются в настоящее время в нескольких языках (c#, kotlin, rust, python, js, c++), чтобы писать асинхронный код в синхронном стиле. Синтаксис async-await впервые появился в C# 5.0 (2012 г.).

В питоне корутины основаны на механизме генераторов, используют их способность приостанавливать свое выполнение и возвращать управление в вызывающий код, а затем возобновлять работу с воссоздпным окружением.

yield yield from
Сложный генератор, давайте его порефакторим и выделим какую-то часть в другой, вложенный, генератор.
def test_gen():
    val = yield 1
    # -----
    yield 2
    yield 3
    yield 4
    # -----
    yield 5
    return 'returned by gen'

gen = test_gen()
try:
    while True:
        print(next(gen))  # = send(None)
except StopIteration as e:
    print(e)

# 1
# 2
# 3
# 4
# 5
# returned by gen
Выделяем вложенный генератор и вызываем его при помощи инструкции `yield from`, которая будет прокручивать вложенный генератор, как в примере слева, и пробрасывать данные, переданные методом `send(smth)`, из родительского во вложенный, прокрутит вложенный генератор до конца и вернет значение из StopIteration, которое мы сохраним в переменную `a`.
def test_subgen():
    yield 2
    yield 3
    yield 4
    return 'returned by sub_gen'
    
def test_gen():
    yield 1
    a = yield from test_subgen()
    print(a)
    yield 5
    return 'returned by gen'

gen = test_gen()
try:
    while True:
        print(next(gen))  # = send(None)
except StopIteration as e:
    print(e)

# 1
# 2
# 3
# 4
# returned by sub_gen
# 5
# returned by gen
Функции Генераторы Корутины
Обычная функция
def some_name(arg1, arg2):
    return arg1 + arg2
Функция-генератор
def some_name(arg1, arg2):
    yield arg1
    yield arg2
    return arg1 + arg2
Функция-корутина
async def some_name(arg1, arg2):
    await asyncio.sleep(arg1)
    await asyncio.sleep(arg2)
    return arg1 + arg2
>>> type(some_name)
<class 'function'>
>>> type(some_name)
<class 'function'>
>>> type(some_name)
<class 'function'>
возвращается значение
>>> some_name(1,2)
3
возвращается объект генератора
>>> some_name(1,2)
<generator object some_name at 0x10d48b2e0>
возвращается объект корутины
>>> some_name(1,2)
<coroutine object some_name at 0x10dfa0a40>
>>> type(some_name(1,2))
<class 'int'>
>>> type(some_name(1,2))
<class 'generator'>
>>> type(some_name(1,2))
<stdin>:1: RuntimeWarning: coroutine 'some_name' was never awaited
Object allocated at (most recent call last):
File "<stdin>", lineno 1
<class 'coroutine'>
>>> dir(some_name(1,2))
# лишнее вырезала
['__iter__', '__next__', 'close', 
 'gi_code', 'gi_frame', 'gi_running', 
 'gi_yieldfrom', 'send', 'throw']

имеет интерфейс итератора

>>> dir(some_name(1,2))
# лишнее вырезала
['__await__', 'close', 'cr_await', 
 'cr_code', 'cr_frame', 'cr_origin', 
 'cr_running', 'send', 'throw']

awaitable-объект

локальные переменные на каждой итерации

>>> def some_name(arg1, arg2):
...    x = 0
...    yield arg1
...    x += arg1
...    yield arg2
...    x += arg2
...    return arg1 + arg2

>>> gen = some_name(1,2)
>>> print(gen.gi_frame.f_locals)
>>> gen.send(None)  # то же самое, что и next(gen)
>>> print(gen.gi_frame.f_locals)
>>> gen.send(None)  # то же самое, что и next(gen)
>>> print(gen.gi_frame.f_locals)
>>> gen.send(None)  # то же самое, что и next(gen)

{'arg1': 1, 'arg2': 2}
{'arg1': 1, 'arg2': 2, 'x': 0}
{'arg1': 1, 'arg2': 2, 'x': 1}
Traceback (most recent call last):
    File "/Users/vera/dev/awaitable1.py", line 93, in <module>
        gen.send(None)
StopIteration: 3

возвращаемое значение находится в объекте исключения StopIteration

локальные переменные на каждой итерации

>>> async def some_name(arg1, arg2):
...     x = 0
...     await other_coro(arg1)
...     x += arg1
...     await other_coro(arg2)
...     x += arg2
...     return arg1 + arg2

>>> gen = some_name(1,2)
>>> print(gen.cr_frame.f_locals)
>>> gen.send(None)
>>> print(gen.cr_frame.f_locals)
>>> gen.send(None)
>>> print(gen.cr_frame.f_locals)
>>> gen.send(None)

{'arg1': 1, 'arg2': 2}
{'arg1': 1, 'arg2': 2, 'x': 0}
{'arg1': 1, 'arg2': 2, 'x': 1}
Traceback (most recent call last):
    File "/Users/vera/dev/awaitable1.py", line 93, in <module>
        gen.send(None)
StopIteration: 3

возвращаемое значение находится в объекте исключения StopIteration. такой же генератор по сути, только вместо gi_frame - cr_frame. gen.send(None) в реальной жизни мы не делаем руками, его вызывает сам event loop.

>>> def coro():
...     y = yield from a
...
>>> dis.dis(coro)
2           0 LOAD_GLOBAL              0 (a)
            2 GET_YIELD_FROM_ITER
            4 LOAD_CONST               0 (None)
            6 YIELD_FROM
            8 STORE_FAST               0 (y)
            10 LOAD_CONST              0 (None)
            12 RETURN_VALUE
>>> async def async_coro():
...     y = await a
... 
>>> dis.dis(async_coro)
2           0 LOAD_GLOBAL              0 (a)
            2 GET_AWAITABLE
            4 LOAD_CONST               0 (None)
            6 YIELD_FROM
            8 STORE_FAST               0 (y)
            10 LOAD_CONST              0 (None)
            12 RETURN_VALUE

Оптимизация асинхронных http-серверов ^

В библиотеке asyncio есть много чего, нет только реализации http-протокола (придется дополнительно устанавливать одну из библиотек - aiohttp, sanic, tornado или подобные), которая подразумевала бы работу с объектами HTTPRequest (полученным парсингом строки запроса) и HTTPResponse (который надо преобразовать в строку ответа, xml или json).

Например, в нижеприведенном запросе http-сервер должен распарсить большую строку от клиента и выделить из нее запрашиваемый метод и путь, query- и path-параметры, заголовки и тело в json, xml или form-параметрах.

vera@vera$ ncat -C --ssl github.com 443
POST /
User-Agent: vera

data=1&n=2

HTTP/1.1 400 Bad Request
Cache-Control: no-cache

{
    "error": 400
}

Чтобы отдать большую строку ответа, нужно сформировать ее из объекта, содержащего возвращаемый код, заголовки, тело ответа в json, xml или html.

Строковые операции всегда дорогие, а для асинхронного приложения это критично. В tornado парсинг написан на питоне https://github.com/tornadoweb/tornado/blob/master/tornado/httputil.py, в aiohttp на cython https://github.com/aio-libs/aiohttp/blob/master/aiohttp/_http_parser.pyx, sanic же использует, помимо uvloop, httptools - биндинги для http-парсера, используемого в nodejs, а также ujson для работы с json - все это написано на C.

Tornado проигрывает последним двум серверам не только из-за большого количества легаси-кода, который до сих пор остается в библиотеке, но также и потому, что нет таких оптимизаций, а Sanic по результатам некоторых тестов показывает наилучшие результаты.

Дополнительные материалы ^