На днях по работе потребовалось сделать утилиту, которая прямо вот из консоли ходит в апи нашего клауд сервиса и берет оттуда кое-какую информацию. Подробности что и зачем - вне этого рассказа. Принципиальный вопрос здесь другой - скорость. Скорость реально важна (порядок количества запросов - десятки и сотни). Потому что ждать - не кайф.
Здесь я хочу поделиться своим ресёрчем на тему запросов, как делать круто, а как нет. С примерами кода конечно. А так же рассказать, как я тупил.

Есть только я и неприятности
Есть только я и неприятности

Начнем, пожалуй, с классики

Последовательные синхронные запросы. Будем использовать всем известную либу requests и tqdm для красивого вывода в консоль. В качестве игрушечного примера выбрал первую-попавшуюся публичную апишку: https://catfact.ninja/. Метрикой качества будет RPS (Request per second). Чем выше - тем соотвественно лучше.

import time
import requests
from tqdm import tqdm

URL = 'https://catfact.ninja/'


class Api:
    def __init__(self, url: str):
        self.url = url

    def http_get(self, path: str, times: int):
        content = []
        for _ in tqdm(range(times), desc='Fetching data...', colour='GREEN'):
            response = requests.get(self.url + path)
            content.append(response.json())
        return content


if __name__ == '__main__':
    N = 10
    api = Api(URL)

    start_timestamp = time.time()
    print(api.http_get(path='fact/', times=N))

    task_time = round(time.time() - start_timestamp, 2)
    rps = round(N / task_time, 1)
    print(
        f"| Requests: {N}; Total time: {task_time} s; RPS: {rps}. |\n"
    )

Получаем следующий вывод в терминале:

Fetching data...: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 10/10 [00:06<00:00,  1.52it/s]
[{'fact': 'Despite imagery of cats happily drinking milk from saucers, studies indicate that cats are actually lactose intolerant and should avoid it entirely.', 'length': 148}, {'fact': 'The smallest pedigreed cat is a Singapura, which can weigh just 4 lbs (1.8 kg), or about five large cans of cat food. The largest pedigreed cats are Maine Coon cats, which can weigh 25 lbs (11.3 kg), or nearly twice as much as an average cat weighs.', 'length': 249}, {'fact': 'A cat has 230 bones in its body. A human has 206. A cat has no collarbone, so it can fit through any opening the size of its head.', 'length': 130}, {'fact': "Cats' hearing is much more sensitive than humans and dogs.", 'length': 58}, {'fact': 'The first formal cat show was held in England in 1871; in America, in 1895.', 'length': 75}, {'fact': 'In contrast to dogs, cats have not undergone major changes during their domestication process.', 'length': 94}, {'fact': 'Ginger tabby cats can have freckles around their mouths and on their eyelids!', 'length': 77}, {'fact': 'Cats bury their feces to cover their trails from predators.', 'length': 59}, {'fact': 'While it is commonly thought that the ancient Egyptians were the first to domesticate cats, the oldest known pet cat was recently found in a 9,500-year-old grave on the Mediterranean island of Cyprus. This grave predates early Egyptian art depicting cats by 4,000 years or more.', 'length': 278}, {'fact': 'Relative to its body size, the clouded leopard has the biggest canines of all animals’ canines. Its dagger-like teeth can be as long as 1.8 inches (4.5 cm).', 'length': 156}]
| Requests: 10; Total time: 6.61 s; RPS: 1.5. |

RPS - 1.5. Очень грустно. У меня еще и интернет не самый быстрый сейчас дома. Ну тут добавить нечего.

Что можно оптимизировать уже сейчас? Ответ: использовать requests.Session
Eсли делать несколько запросов к одному и тому же хосту, базовое TCP-соединение будет использоваться повторно, что приводит к значительному увеличению производительности. (цитата из документации requests)

Используем сессию

    def http_get_with_session(self, path: str, times: int):
        content = []
        with requests.session() as session:
            for _ in tqdm(range(times), desc='Fetching data...', colour='GREEN'):
                response = session.get(self.url + path)
                content.append(response.json())
        return content

Немного изменив метод, и вызвав его, видим следующее:

Fetching data...: 100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 10/10 [00:02<00:00,  3.88it/s]
[{'fact': 'Cats purr at the same frequency as an idling diesel engine, about 26 cycles per second.', 'length': 87}, {'fact': 'Cats eat grass to aid their digestion and to help them get rid of any fur in their stomachs.', 'length': 92}, {'fact': 'A cat’s heart beats nearly twice as fast as a human heart, at 110 to 140 beats a minute.', 'length': 88}, {'fact': 'The world’s rarest coffee, Kopi Luwak, comes from Indonesia where a wildcat known as the luwak lives. The cat eats coffee berries and the coffee beans inside pass through the stomach. The beans are harvested from the cat’s dung heaps and then cleaned and roasted. Kopi Luwak sells for about $500 for a 450 g (1 lb) bag.', 'length': 319}, {'fact': 'There are more than 500 million domestic cats in the world, with approximately 40 recognized breeds.', 'length': 100}, {'fact': 'Cats sleep 16 to 18 hours per day. When cats are asleep, they are still alert to incoming stimuli. If you poke the tail of a sleeping cat, it will respond accordingly.', 'length': 167}, {'fact': 'Since cats are so good at hiding illness, even a single instance of a symptom should be taken very seriously.', 'length': 109}, {'fact': 'At 4 weeks, it is important to play with kittens so that they do not develope a fear of people.', 'length': 95}, {'fact': 'The technical term for a cat’s hairball is a “bezoar.”', 'length': 54}, {'fact': 'Baking chocolate is the most dangerous chocolate to your cat.', 'length': 61}]
| Requests: 10; Total time: 2.58 s; RPS: 3.9. |

Почти 4 RPS, в сравнении с 1.5 уже прорыв.
Но не секрет, что для сокращения i/o time есть практика использования асихнронных/многопоточных программ. Это как раз такой случай, потому что во время ожидания ответа от сервера наша программа ничего не делает, хотя могла бы отправлять уже другой запрос, а потом другой и т.д. Попробуем реализовать асинхронный подход к решению кейса.

async / await

Для удобства вызовов сделаем функцию-оболочку:

def run_case(func, path, times):
    start_timestamp = time.time()
    
    asyncio.run(func(path, times))

    task_time = round(time.time() - start_timestamp, 2)
    rps = round(times / task_time, 1)
    print(
        f"| Requests: {times}; Total time: {task_time} s; RPS: {rps}. |\n"
    )

И собственно сама реализация метода (не забудьте поставить aiohttp, обычные реквесты не работают в асинхронной парадигме):

    async def async_http_get(self, path: str, times: int):
        async with aiohttp.ClientSession() as session:
            content = []
            for _ in tqdm(range(times), desc='Async fetching data...', colour='GREEN'):
                response = await session.get(url=self.url + path)
                content.append(await response.text(encoding='UTF-8'))
            return content
if __name__ == '__main__':
		N = 50
    api = Api(URL)
    run_case(api.async_http_get, path='fact/', times=N)

Видим:

Async fetching data...: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:12<00:00,  3.99it/s]
| Requests: 50; Total time: 12.55 s; RPS: 4.0. |

Удивительно, но разницы с предыдущим случаем особо нет. Я думаю выигрыш в i/o тайме компенсируется издержками на передачу управления потоком функциями друг другу (слышал что очень ругают эти моменты в python). Как на самом деле не знаю.

На словах такой подход можно объяснить так.
- В цикле создается корутина, которая отправляет запрос.
- Не дожидаясь ответа, управление потоком отдается снова event loop'у, который создает следующую по "for циклу" корутину, которая тоже отправляет запрос.
- Но теперь прежде чем отдать управление эвент лупу проверяется статус ответа первой корутины. Если она может быть продолжена (получила ответ на запрос), то управление потоком возвращается ей, если нет, то см. пункт 2.
- И так далее

В итоге код-то впринципе асихнронный, но запросы не отправляются "разом". Принципиально иначе подойти к этой ситуации поможет asyncio.gather.

Используем asyncio.gather

Gather - как ни банально с английского собирать. Метод gather собирает коллекцию корутин и запускает их разом (тоже условно конечно). То есть, в отличии от предыдущего случая, мы в цикле создаем корутины, а потом их запускаем.

Было:
[cоздали корутину] -> [запустили корутину] -> [cоздали корутину] -> [запустили корутину] ->
[cоздали корутину] -> [запустили корутину] ->[cоздали корутину] -> [запустили корутину]

А стало:
[cоздали корутину] -> [создали корутину] ->[cоздали корутину] -> [создали корутину] ->
[запустили корутину] -> [запустили корутину] -> [запустили корутину] -> [запустили корутину]

    async def async_gather_http_get(self, path: str, times: int):
        async with aiohttp.ClientSession() as session:
            tasks = []
            for _ in tqdm(range(times), desc='Async gather fetching data...', colour='GREEN'):
                tasks.append(asyncio.create_task(session.get(self.url + path)))

            responses = await asyncio.gather(*tasks)
            return [await r.json() for r in responses]
if __name__ == '__main__':
		N = 50
    api = Api(URL)
    run_case(api.async_gather_http_get, path='fact/', times=N)

И получаем... получаем... ничего не получаем. Курсор продолжает многозначительно мигать в окне терминала. Не работает - подумал Штирлиц.

Путем мучительного дебага и попыток понять, почему мой код не работает, я понял - причина в моем VPN. Его узлы находятся где-то в юрисдикции Cloudflare. А они такое поведение не поощряют, считая, что я бот. Нормальный человек столько запросов в секунду делать не будет, поэтому мои запросы...теряются где-то в пучинах интернета. Ответа на них не будет. Никогда. Корутины просто не заканчиваются.

Окей, поняв откуда ноги растут, запускаем код:

Async gather fetching data...: 100%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 50/50 [00:00<00:00, 106834.03it/s]
| Requests: 50; Total time: 2.38 s; RPS: 21.0. |

Цифры выросли, круто, 21 не 1.5 - это уж точно.
Однако же есть какой-то предел, увеличим N (число запросов) до 200.

| Requests: 200; Total time: 2.97 s; RPS: 67.3. |

Это что же получается, можно и так? На самом деле нет. Если внимательно рассмотреть, что же все-таки нам отвечает сервер, то заметим, что большая часть ответов это {'message': 'Rate Limit Exceeded', 'code': 429}

Конкретный лимит запросв, который я установил в ходе эксперимента с этим сервисом - это около 60 ответов за раз, остальное он не переваривает. Так что если будете так ходить в сервисы, которые не хотите перегрузить или вообще положить, то подходите к этом вопросу обдуманно, не превышайте определенных рамок.

Что там с threading?

Тут особо смысла нет - практический эксперимент показал, что threading показывает такие же результаты (плюс - минус), как асихнронный код из пункта 3.

multiprocessing

Не буду врать, просто посмотрел как нечто похожее делал какой-то индус с Ютуба. Результаты сильно хуже, чем у предыдущих способов. Да и писать такой код - это насилие над своей психикой. А я свою психику берегу.

Подведу итоги:

  • На днях упал Cloudlfare - извините, это из-за меня, больше не буду.

  • Хотите быть чемпионом по запросам - используйте asyncio.gather, но с тщательно подобранными лимитами. Если вы ходите не на один хост, а в разные источники, то вообще не стестяйтесь.

Комментарии (26)


  1. lebedec
    29.06.2022 19:31
    +37

    Не понимаю за что плюсы. Автор излагает свои поверхностные знания об асинхронном программировании, делает неверные выводы, а ведь кто-то эту статью будет использовать как учебное пособие. 

    Что там с threading?

    Тут особо смысла нет - практический эксперимент показал, что threading показывает такие же результаты (плюс - минус), как асихнронный код из пункта 3.

    multiprocessing

    Не буду врать, просто посмотрел как нечто похожее делал какой-то индус с Ютуба. Результаты сильно хуже, чем у предыдущих способов. Да и писать такой код - это насилие над своей психикой. А я свою психику берегу.

    Языковые конструкции async await — это просто синтаксический сахар. Значение имеет только конкретная реализация и конфигурация под капотом этих конструкций.  

    Автор использовал дефолтную реализацию в Python — на ивент лупе. Любые вычисления на CPU будут блокировать весь поток и съедать весь выигрыш на асинхронных операциях чтения из неблокируемых сокетов. Внезапно, к этим вычислениям относится даже json десериализация.

    Поэтому если говорить о реальных RPS, которые измеряются тысячами, а не десятками, без мультипоточности или мультипроцессорности не обойтись. Ниакой asyncio.gather тут не поможет.


    1. stepacool
      29.06.2022 20:22
      +2

      У меня искренний вопрос, а как мультипоточность может дать выигрыш над асинхронностью? Треды также будут ограничены ресурсом CPU же? Или я что-то не так понимаю?


      1. Vindicar
        29.06.2022 20:46
        +5

        Асинхронный цикл работает строго в одном потоке, и обслуживает одну корутину за раз. Ожидать выполнения операции ввода-вывода может несколько, но CPU потребляет только одна корутина.
        В случае многопоточности гипотетически можно добиться выигрыша на многоядерных машинах (с поправкой на проблему GIL — вроде есть реализации Питона, которые ей не страдают).
        В случае многопроцессности GIL уже тоже не проблема.
        Т.е. если у нас почти чистый ввод-вывод — то выигрыш многопоточности будет минимален. Если есть солидная доля CPU-активности — разница будет.

        Ну а совсем до кучи питон из коробки позволяет «спрятать» процесс/поток в асинхронную задачу, что здорово размывает границы между подходами.


      1. lebedec
        29.06.2022 21:35
        +13

        Давайте разберёмся.  

        Асинхронность — свойство операции, которое означает что время выполнения этой операции принципиально не совпадает с ходом времени вашей программы, можно сказать нелинейно. Как правило — это длительные и не дискретные операции: пользовательский ввод, календарное планирование, долгий запрос в базу данных или внешний сервис.

        Многопоточность — свойство системы, которое позволяет выполнять несколько операции параллельно, то есть одновременно. В современных компьютерах это физически параллельные вычисления на разных ядрах CPU (отсюда такая гонка за количеством ядер и феноменальная производительность GPU на тензорных операциях).  

        Напрямую противопоставлять эти свойства некорректно. 

        Да, дефолтная реализация в Python или JavaScript использует ивент луп - то есть цикл с диспетчеризацией неблокируемых и последовательных операций. Ключевой момент здесь — последовательных. Заметное увеличение производительности происходит только за счёт того, что приложение продолжает выполнение других задач пока ожидает ответ от подсистемы IO (например поток байт из сокета). Но параллельных вычислений при этом не происходит, а значит компьютер не использует все свои вычислительные мощности.

        Но, асинхронное программирование можно реализовать, как угодно. Например, в C# дефолтная реализация асинхронщины использует разогретый пул потоков, читай многопоточность. В Python тоже можно запускать async/await на потоках, через какой-нибудь ThreadPoolExecutor. 

        Таким образом максимальную производительность вы можете получить, комбинируя неблокируемые операции и параллельные вычисления. Конфигураций можно придумать миллион. В одном потоке принимать в расшаренную память данные из сокетов, а в другом их обрабатывать в векторе с кэшем процессора. Ну или запускать несколько ивент лупов, потому что на больших количествах RPS начинают играть даже накладные расходы на диспетчерезацию внутри цикла (так, все асинхронные Python фреймворки в продакшене работают через ранеры которые, сюрприз-сюрприз, запускают несколько потоков или даже процессов для выполнения вашего асинхронного кода).

        Использовать при этом сахарный async/await или долбить всё на колбеках — это уже вам решать. Главное, что от физики не уйти, нужно понимать, как работает железо под копотом, о том и речь. 


        1. stepacool
          29.06.2022 22:19

          У меня такое представление:
          Треды/Потоки тоже не исполняются одновременно, разница лишь в том, что потоки - "рандом", и на уровне ОС решается, какой поток должен исполняться в конкретный промежуток времени(кроме Greenlet), а асинхронность - да, цикл и прочее, но тоже одна задача в промежуток времени исполняется, просто всё решается на уровне приложения.

          Я не слышал о таком, чтобы потоки были действительно параллельны, как процессы. Именно параллельны, то есть одновременно исполяющиеся, а не последовательны. Может есть какие-то бонусы от памяти/контекста исполнения, но используя 100% CPU что асинк, что многопоточность будут примерно одинаковы, ибо на самом деле делают считай одни и те же вещи - "прыгают" от ожидающей задачи к готовой к исполнению. Просто у асинка все делает eventloop и реализация приложения/языка, а у тредов - ОС(кроме зеленых тредов).

          И собственно говоря мой вопрос отсюда и возник - где треды берут "лишнюю" работу ЦПУ? От физики не уйти, как вы сказали. И все будет ограничено железом ну и оверхедом от реализаций/вызовов и тп.

          П.С про процессы все понятно, к ним вопросов нет. Но даже они обычно - воркеры, считай копии приложения, а не подресурс в одном контексте исполнения(зависит от приложения, конечно).


          1. lebedec
            29.06.2022 22:47
            +5

            Ваше представление сложилось так, потому что "поток" — тоже определенная абстракция, за которой стоят детали реализации. Стоит разобраться, о чем именно мы говорим. 
             
            "Системные потоки" — то есть потоки управляемые ОС, могут выполняться буквально и физически параллельно на разных ядрах процессора или разных процессорах. В рамках одного системного процесса или нет. Для этого и придумывают многоядерные процессоры, многопроцессорные суперкомпьютеры, тензорные ядра и сопроцессоры.  
             
            "Виртуальные потоки" — разного рода легкие|тонкие|зелёные потоки, тоже могут выполняться параллельно. Представьте что ваша виртуальная машина умеет компоновать несколько виртуальных операций в одну физическую и исполнять за один такт физического процессора, тогда потоки выполнятся параллельно даже на одном ядре. Для этого придумывают всякие Erlang'и. 
             
            Проблема планирования времени выполнения этих потоков возникает тогда, когда физический ресурс меньше, чем логический. Если на вашем компе 128 ядер, а системных потоков 512, тогда да, не все из них будут физически одновременно работать, будет оверхед на переключение и так далее.  
             
            Об этом и речь. Возвращаясь к вашему изначальному вопросу. Суть не в том, что потоки берут откуда-то дополнительную работу ЦПУ. Суть в том, что дефолтное асинхронное приложение на ивентлупе с одним потоком не утилизирует многоядерный ЦПУ на 100%


            1. stepacool
              29.06.2022 23:10

              В своём опыте не встречал, к сожалению, кросс-процессорных/ядровых/процессовых потоков, все всегда было в рамках одного процесса, выше - только уже воркеры и тп, максимально за абстракциями. Что-то уровня 1 ядро = 1 процесс и около того.

              На уровне одного процесса у асинка меньше оверхед, и он даст результат лучше в плане RPS и прочего.

              UPD. в случае многоядерного ЦПУ - просто много асинхронных воркеров под количество ядер, вот и утилизация на 100%. Думаю даже оверхед выиграет.

              Продвинутые(в моём понимании) виды параллельности, что вы описали, как компановка операций в один тик, межпроцессовые треды в рамках одной задачи и тп думаю в самом деле перебьют асинк, ибо "пул" больше, но в стандартном вебе такого не встречал.

              Спасибо за разъяснения, узнал новое для себя.


              1. funca
                30.06.2022 00:01

                Треды это больше про отзывчивость (десктоп, GUI, игры там разные), нежели масштабирование.

                С одной стороны у нас находится огромный сегмент задач, которые прекрасно выполняются в один поток (включая кооперативную многозадачность a'la asyncio). С другой: хайлоад, высокая доступность, числодробилки - где про масштабирование уже думают хостами, availability zone и дата центрами. И где-то между ними находится сегмент задач, которые укладываются в одну машинку, но могут получить преимущества от небольшого кратного распараллеливания (сколько там у вас ядер?).

                Одна из распространенных проблем, которую разработчики на python пытаются решать с помощью тредов, это блокирующий ввод-вывод. Но по сути это костыли, ввиду отсутствия удобных интерфейсов. Хотя есть же twisted или rxpy, которые скрывают от приложения всю такую ерунду.


        1. grongg
          29.06.2022 22:27
          +3

          С generic точки зрения-то все так, но конкретно в питоне существование GIL (в CPython, который стандарт де-факто, как ни крути) нельзя игнорировать -- в python web фреймворках раннеры на тредах делают только в том случае, если код хендлеров написан не в async манере / если используются бинарные расширения (например, numpy, который вообще держит свой пулл openmp потоков под капотом), отпускающие GIL. То же самое -- использование TheadPoolExecutor не даст прироста, если не отпускать GIL в бинарных расширениях. Вообще с раннерами же правило большого пальца -- делать все на процессных воркерах -- и жизненный цикл воркеров с мастером развязан (можно киллять без зазрения совести), и утилизация cpu из коробки нормальная. Единственная проблема -- ботлнек в общении с мастер-процессом, но это мизер и там где это надо учитывать в принципе не надо писать на питоне.

          Со всем остальным согласен :+1:


  1. Tanner
    29.06.2022 19:40

    А можно было использовать Scrapy, там параллельные запросы и всякие лимиты из коробки.


  1. Murtagy
    29.06.2022 20:13
    +9

    Освоил документацию к асинхронщине и уже бандит, что еще впереди...


  1. Stormwalker
    29.06.2022 20:16
    +6

    Маловато, даже до семафоров не дошли


  1. chaotism
    29.06.2022 20:23
    +2

    Вариант с async/await у вас работает в цикле, в программе где одна функция, поэтому предсказуемо работает так же быстро как и синхронный код. gather/wait/asyncio.run хорошо помогают в этом случае, как и показал ваш пример ниже.


  1. grongg
    29.06.2022 20:24
    +11

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

    Не дожидаясь ответа, управление потоком отдается снова event loop'у, который создает следующую по "for циклу" корутину, которая тоже отправляет запрос.

    В вашем первом примере с async-await-oм ничего такого не происходит. Следующая итерация цикла не начнется, до тех пор, пока мы не вывалимся (последовательно) из обоих await-ов.

    • await coroutine -- синтаксический сахар над await from iterable. В этот момент передачи управления event-loop-у не происходит -- мы сразу проваливаемся в корутину

    • корутина session.get() через цепочку await-ов внутри себя в какой-то момент блокируется (например, на ожидание сокета). Все это происходит через asyncio методы, поэтому он про все знает -- и помечает всю текущую task как ожидающую сокет (там все немного интереснее, но верхнеуровнево -- так), все это дело засыпает и контроль передается event-loop-у

    • В event-loop-е в этот момент нет никаких готовых к запуску здесь и сейчас task (есть всего одна и та в режиме ожидания)

    • Когда сокет готов, asyncio передает управление таске -- мы вылетаем из await-а

    • так, последовательно мы доходим до конца итерации цикла и начинаем следующую -- только тогда и начнем второе хождение по http

    Доп. моменты, которые стоило бы обсудить:

    • Все, таки, что с тредингом и  multiprocesing -- упомянуто три четыре способа, один из которых неправильный, а два -- "ну это пропустим". Multiprocessing вполне себе валидный инструмент в определенных контекстах.

    • rate limit -- подумали бы, как сделать красивое ограничение на частоту запросов

    • какие минусы вашего решения, если нужно обкачать много-много запросов, но с низким рейт лимитом (подсказка -- в event-loop будут создаваться ненужные task-и)

      • как поступать в таком случае (подсказка -- asyncio очередь с воркерами)


    1. LookingConfused Автор
      29.06.2022 20:26

      Спасибо за развернутый комментарий. Покурю материал на эту тему и попробую дополнить/переделать статью.


      1. grongg
        29.06.2022 20:51
        +3

        С документацией одна беда -- она не user-friendly и не для начинающего входить в эту тему, для меня ее полезность стала актуальной только когда я полез в исходники asyncio. На своем курсе по Python я активно рекламирую цикл уроков от bbc https://bbc.github.io/cloudfit-public-docs/ и вишенкой на торте -- шикарнейшие видеокасты от David Beazley, который пишет свой микро asyncio -- после этого наступает просветление и кристалльное понимание, что никакой магии там не происходит. После -- разбор концепций структурного асинхронного программирования (trio / anyio), потом депрессия от понимания, что асинхронность в Python безвозвратно продолбана и наконец -- посыл Python к чертовой матери и переход на Go =D


        1. avorsa
          29.06.2022 21:14

          Расскажите о "своем курсе по Python". Можно в личку. Спасибо.


          1. grongg
            29.06.2022 22:07

            курс в (к сожалению, закрытом недавно) ОзонМастерс


        1. funca
          29.06.2022 23:11

          асинхронность в Python безвозвратно продолбана

          В питоне давно так: активные контрибьюторы создают хайп вокруг своей идеи и пока ни кто не успел опомниться проталкивают сырой код в стандартную библиотеку. А потом уже ни кто не может это исправить. Интерфейс Event Loop , содержащий 100500 публичных методов, очередной яркий тому пример.

          Trio концептуально интересный проект, осмелившихся бросить вызов этому огромному монстру. Но ему не хватает строгой теоретической базы под капотом. Автор тоже скорее думает метафорами из реального мира, поэтому отдельные противоречия вылезают в самых неудобных местах (например как реализована обработка ошибок).


          1. grongg
            29.06.2022 23:41
            +2

            Я плюсовать не могу:) Это точно, что-то когда-то пошло не так. Меня тут напрягают несколько моментов

            • дихотомия sync vs async миров -- из-за разных путей вызовов магических методов в кишках очень сложно писать код, который работал бы и там, и там без копипасты. Мне кажется, если бы одни и те же dunder методы можно было вызывать и в sync режим, и в async -- питон был бы сильно проще. И мне кажется, что корень зла -- генераторное прошлое корутин. Т.е. их сначала сделали, извернувшись, через генераторы, но потом закопали эти генераторы глубоко в имплементацию, добавив async/await сахар поверх, а мб стоило выкинуть генераторы и сделать async/await "на уровне интерпретатора". Но я глубоко про это не думал, это, очевидно, сложная тема + задним числом все умные, как всегда

              • особняком стоит гениальный и настолько же взрывоопасный gevent, который позволяет не разделять миры, но делает это настолько инвазивно, что сидишь все время гадаешь, бомбанет или нет и главное -- в каком месте

              • есть изыски типа https://sobolevn.me/2020/06/how-async-should-have-been , идеи интересные, но я считаю, что такое в прод тащить нельзя =D

            • зоопарк несовместимых друг с другом ивентлупов и завязанность третьесторонних библиотек на конкретные имплементации

            • жару добавляет какая-нибудь реализация gRPC на питоне -- python thread-based сервер поверх cython-а, который поверх сишной либы, у которой свой тред пул.

              • и ты такой пилишь проект на джанге с gevent-ом и сидишь трясешься, не понимая, будет ли gRPC работать на greenlet-ах и можно ли и как это все поженить с какими-нибудь async-либами (у gevent и asyncio ивентлупы не 1-в-1 совместимы), а потом еще думаешь, а классно сделать в trio стиле и мозг в итоге взрывается. Слишком много слоев, которые все надо держать в голове


            1. funca
              30.06.2022 00:18

              Я плюсовать не могу:)

              Напишите уже статью. Вы тут в комментах уже накидали на целый учебник. :)

              В python async/await это такая профанация, что в любой момент с помощью нехитрого декоратора все можно вернуть как было https://github.com/alex-sherman/unsync .


  1. galqiwi
    29.06.2022 23:18
    +4

    Очень советую курс Романа Лисовского по concurrency (многопоточность, асинхронность и многое другое) для тех, кто хочет разобраться подробно в вопросе. https://youtube.com/playlist?list=PL4_hYwCyhAva37lNnoMuBcKRELso5nvBm


    1. grongg
      30.06.2022 00:09
      +1

      Выглядит исключительно годно, спасибо!


  1. Aquahawk
    30.06.2022 10:06
    +1

    Буквально недавно был конкурс от Россельхоз банка на tdconf? Ddos me. Ребята попросили сделать к ним много get запросов. Ну в общем я сделал попрядка 1.3 миллиона реквестов в секунду с одного сервера особо не напрягаясь, и всё упёрлось в их инфраструктуру. Да это слегка хулиганство, но чёрт возьми, на порядок быстрее чем люди у которых стояли многоядерные сервера в 100% загрузке cpu. На nodejs. Код тут https://github.com/Busyrev/gatling/blob/master/src/index.ts учтите, код писался в 2 часа начи между конфой на коленке, это не эталон хорошего кода. Надо бы причесать да написать что-нибудь на эту тему, может быть когда нибудь. Единицы rps это невероятно медленно, это ужасно. Нормальный веб фреймвёрк должен давать ну хотябы десятки тысяч rps вообще без сложностей.


    1. Aquahawk
      30.06.2022 10:53
      +2

      Это я всё зачем написал, чтобы люди понимали что нормальной сетевой производительностью являются миллионы запросов в секунду. На фреймвёрках такое как правило не достижимо, но тем не менее, десятки и сотни тысяч rps это нормальный цифры, ели они они меньше то надо искать проблемы, либо на вызывающей, либо на принимающей стороне. И ну не очень верю я что провайдер будет блочить за какие-то штучные rps, посмотрите сколько запросов уходит когды вы открываете что-то типа facebook. Вот пример где человек осознал что его цифры ооочень странные и вроде как даже нашёл проблему https://habr.com/ru/post/580066/


  1. Cykooz
    01.07.2022 11:59

    Дааа-с, завернуть в gather() отправку запросов на сервер вы догадались, а сделать то же самое с получением тела ответа от сервера - нет. В результате ответы на свои запросы вы по прежнему вычитываете последовательно, один за другим.