Использование корутин в C++20
Всем привет.
Как уже говорилось, я разработчик в Яндексе.
Я занимаюсь распределенной Open Source базой данных в IDB.
Сегодня я вам расскажу про крутины в последнем стандарте C++20.
Все вы, наверное, писали когда-нибудь сетевой код.
Возможно, кто-то из вас писал даже распределенные приложения сложные.
И все знают, что классический подход написания таких приложений — это использование колбеков.
и, возможно, кто-то даже использовал кулбейки, а кто-то использовал фьючи.
По сути, фьючи — это те же кулбейки, завернутые в чуть более удобный интерфейс, и все знают, насколько сложно писать такой код и насколько сложно его читать, потому что вы посылаете сообщение, у вас есть обработчик
по ссылке сообщения, обработчик получения ответа, разные обработчики находятся в разных частях кода, и вам надо постоянно глазами идти то в одну часть кода, то в другую часть кода, это сложно читать, потом отлаживать.
И вот карутины, они позволяют писать такой асинхронный код,
в обычном плоском виде.
То есть он выглядит как синхронный, но на самом деле асинхронный.
О чем я буду рассказывать?
Вначале я расскажу о том, какие возможности предоставляют язык C++ для поддержки карутины.
Потом мы напишем небольшой карутинный framework.
Далее я расскажу про продвинутое использование карутины.
В конце покажу тесты своей библиотеки.
Корутины добавлены были в стандарт языка C++20, что это вообще такое?
Корутины — это такая штука, которая позволяет приостанавливать выполнение функции и потом возобновлять его.
Вот, всё достаточно просто.
И почему используются корутины?
Они, например, 3D, потому что корутины по сравнению с 3D они достаточно легковесные.
карутины в языке C++, они стеклос, что это означает, это означает, что
стейт карутины состоит из регистров процессора и локальных переменных.
А для пользователя карутины это значит, что приостанавливать карутину можно только внутри самой карутины.
Но нельзя вызвать функцию в карутине и в этой функции сделать приостановку.
Такое не поддерживается, потому что
стек, карутины C++20 не имеют.
Они переиспользуют стек вызывающего кода.
Вот для поддержки карутин в стандарт были введены следующие ключевые слова.
Во-первых, это Coivate.
который позволяет приостанавливать карутину и далее запускать ее.
CoELT, который позволяет возвращать очередное значение из карутины.
CoELT нужен для поддержки генераторов.
Про генераторы я сегодня рассказывать вообще не буду ничего.
Вот такой ретурн.
Это финальный оператор, который позволяет возвратить значение из карутины и завершить ее работу.
Когда идет речь о карутинах, следует выделить три основные темы.
Первая тема — это поддержка
Базовых примитивов C++ предоставляет три примитива работая с карутиными.
Это карутин-хендл, с помощью которого можно управлять карутиной в вызывающем коде.
Это промис, с помощью которого задается поведение карутины.
И awaitable, с помощью которого можно реализовывать асинкронные операции.
Сам язык C++ и его стандартной библиотека никаких карутина не реализуют.
С помощью этих базовых трёх механизмов можно реализовать тот или иной вариант карутины.
Здесь я выделяю следующие варианты.
Во-первых, это синкавейт карутины.
Под этим я понимаю карутины, которые не возвращают никакого результата и внутри которых только делаются вызовы о синхронных операциях.
Дальше таск-бейст карутины.
Это более сложные карутины.
Они позволяют вернуть результат, и мы можем этого результата как-то дождаться.
Также карутины можно классифицировать как ленивые и жадные.
Ленивые карутины при создании
они создаются сразу в остановленном состоянии, потом их нужно запустить отдельно руками, ажадные они сразу при создании начинают исполнение своего кода.
Также с помощью базовых примитивов языка C++ можно реализовать такие вещи, как All и Any.
All это значит дождаться результата всех каротин,
каких-то лежащих, например, в каком-то векторе, а ЭНИ – это дождаться результата какой-то одной из этого вектора.
Вот и так же, когда мы работаем с карутинами, понятно, что их нужно где-то исполнять в каком-то экзекьюторе,
их нужно планировать и уметь связывать с ивентлупом.
Вот эти все темы так или иначе мы сегодня разберем и начнем мы с базовых примитивов языка C++.
Перед этим рассмотрим такую простейшую корутина, вот первая простейшая корутина она ничего осмысленного не делает, она только два раза засыпает,
Давайте посмотрим, как это работает.
Мы в Main не создали карутину с помощью вызова карутина.
Карутина начала исполняться.
Это у меня жадная карутина, которая сразу исполняется.
Мы доходим до третьей строки, до первого коэвейта.
Карутина засыпает на suspend always.
И сразу исполнение кода переключается на девятую строчку назад в Main.
И в Main нам приходит
объект типа «таск».
И с помощью этого объекта мы уже можем управлять карутиной.
Например, руками ее пробудить.
Мы доходим до десятой строчки, пробуждаем карутину, то есть дергаем резьум.
И в этот момент исполнение опять прыгает назад на третью строчку, мы просыпаемся, доходим до четвертой и опять засыпаем.
И вот таким образом
Мы прыгаем постоянно то в одну сторону, то в другую сторону.
А что это похоже?
Что это напоминает?
В языке C и C++ есть такая штука, как SetJump, LongJump для реализации нелокального GoTo.
Вот это немножко напоминает этот механизм.
По сути своей карутины это есть нелокальный GoTo, только безопасный.
С каждой карутиной связаны две сущности.
Это карутин-хэндел и промес.
С помощью карутин-хэндла мы управляем карутиной извне карутины, а с помощью промеса изнутри.
Но этот карутин-хендл, пользователь карутины, возвращается не напрямую, а завернутый в какой-то другой класс.
Вот в данном случае он завернут в t-таск.
И куда его заворачивать, определяется с помощью метода get return object на восьмой строке класса promise.
Зачем это нужно?
указалось бы, почему нельзя просто Caroutine Handle вернуть и его использовать.
Ну, потому что с помощью этих базовых примитивов языка можно реализовывать какую-то библиотеку.
Вдруг пользователь библиотеки не устраивает интерфейс, предоставляемый Caroutine Handle.
И мы хотим накрутить какой-то свой собственный интерфейс, поэтому возвращается не просто Caroutine Handle, а Caroutine Handle
завёрнутый во что-то.
На самом деле, можно просто таск возвращать, а корутин хэндл куда-то в другое место прикопать, потому что на девятой строке вот этот код, возвращающий таск, может быть сколь угодно сложный.
Что у нас ещё есть в промисе?
У нас в промисе есть два метода initial suspend и final suspend, которые определяют поведение корутины на старте и при завершении.
Здесь возвращается политика suspendа.
В данном случае Initial Suspend возвращается Suspend Never.
Это значит, что при старте карутина сразу начнет работать.
Но здесь мы можем вернуть, например, Suspend Always или какую-то свою кастомную политику.
Зачем это нужно?
Можно, например, создавать карутины в спящем состоянии, а потом передавать их дальше на исполнение в какой-то наш кастомный экзекьютор, например, в Trade Pool.
И там их уже будить и исполнять.
Есть еще Final Suspend.
Final Suspend зовется после того, как карутина вызвала Cure Return и вернула результат.
Вот здесь опять мы в Final Suspend можем либо заснуть, либо не заснуть, либо сделать что-то сложное.
Если мы не засыпаем, то карутина завершает свою работу, промес, карутин-хендл и все, что связано с карутиной, автоматически разрушается.
Можно также заснуть.
Например, если у нас какой-то промес сложный, в нем лежат какие-то значения, которые мы хотим
получить после того, как карутина завершила работу, понятно, что Final Suspend лучше засыпать, чтобы это ничего не разрушилось.
Ну, дальше есть методы Return Void, который зовется, когда карутина дергает Curiton, и Unhandled Exception, который зовется, если внутри карутины бросили исключение и не позвали Trycatch.
В этом случае можно Unhandled Exception использовать для того, чтобы поймать исключение.
Что касается самого карутин-хендла.
Следует иметь в виду, что это не владеющая сущность.
Вот это очень важно.
И если у вас карутина завершилась в спящем состоянии, то кто-то должен на этом карутин-хендле позвать функцию дестрой, чтобы всё разрушить, чтобы не произошло утечки памяти.
Также у карутин-хендла есть три полезные функции.
Это дан, которая позволяет получить статус карутины завершена или не завершена, резьум запускает карутину, ну и диструя, понятно, уничтожает.
Следует иметь в виду, что все эти функции можно дергать только в том случае, если карутина находится в спящем состоянии.
Если она в каком-то другом состоянии, то будет undefined behavior.
Если вам повезет, то программ просто рухнет.
Давайте рассмотрим теперь примеры awaitable.
которые используются для реализации о синхронных операций.
Два таких примитивных примера — suspend always и suspend never.
Каждый awaitable должен содержать три функции — await ready, await suspend, await resume.
Await Radio проверяет, есть у нас результат или нет.
Если результат уже есть, то карутина не засыпает, а просто в этом случае мы вызываем Await Resume и возвращаем результат о синхронной операции.
В данном случае Suspent Always, Suspent Never, они ничего не возвращают, поэтому Await Resume имеет тип Void.
Но понятно, что здесь тип может быть какой угодно, какой вам захочется.
Если результат у нас не готов, то есть, если await ready возвращает false, в этом случае нас позовется await suspend, в который передается хэндл карутины, которая засыпает.
И после этого карутина остановится.
Давайте теперь рассмотрим первые осмысленные weightable, которые делают что-то полезное.
Вот такое weightable, которое называется self.
И пример его использования на 9-ой строке.
Допустим, что нам зачем-то понадобилось из самой карутины получить handle этой карутины.
Как это сделать?
Можно написать такой weightable,
В lightweight ready мы возвращаем сразу false, чтобы у нас позвался weight suspend.
Weight suspend зовется и мы в нем прикапываем handle этой карутины, внутри которой позвали кое weight.
Но здесь следует отметить, что в lightweight suspend мы возвращаем false.
Следует иметь в виду, что evade.spent в avaitable устроен очень сложным образом.
На самом деле здесь может быть реализовано три семантики с помощью одной функции.
Простейший семантик это если evade.spent возвращает void, в этом случае после вызывая evade.spent карутина заснет.
Второй вариант это возвращаем bool.
И если в этом случае возвращается false, то карутина не будет спать.
Если true, то будет спать.
Вот в данном случае мы evade suspend используем только для прикапывания карутин handle внутрь evadeable.
Но карутину не отправляем в сон.
Вот поэтому у нас здесь как бы асинхронная операция, но на самом деле она не асинхронная, потому что мы здесь не засыпаем, а просто прикапываем handle.
И handle потом возвращаем evade resume.
Кажется, что такое вейтабол никогда не понадобится, но на самом деле он мне понадобится позже для реализации одной крутой штуки, поэтому запомните его, пожалуйста.
Собственно, с первой частью на этом всё.
И сейчас, с помощью вот этих базовых вещей, мы попробуем реализовать простейший крутинный фрейворг.
Я, когда реализовывал
корутинный фреймворк отталкивался вот от этого примера.
Что это за пример такой?
Если вы откроете популярный сайт cppreference, откроете статью про корутины, вот это первый пример, который вы
увидите в этой статье.
Это вообще иллюстрирующий пример показывает то, насколько красиво может быть код на карутинах.
Он гипотетический.
Я не знаю, откуда взятые вот эти вот методы AsyncRite, AsyncRid сам, из какой библиотеки в статье на cpp-reference, кажется, это не указано.
Я от этого примера отталкивался, когда писал свой framework, пример реализует эхо-сервер,
Я вот написал такой эх-клиент, вот он написан уже с использованием моей библиотеки, которую мы сейчас разберем.
Здесь создаются два сокета, но первый сокет, это на самом деле не сокет, а эстедын.
Там нолик передается, это хэндл эстедына.
Вот, и здесь мы читаем асинхронный STDN, пишем сокет, читаем из сокета.
И у меня реализованы асинхронные операции write some и read some.
Вот, семантика их такая же, как у syscalls of read and write.
То есть, они используют сколько-то byte из буфера и возвращают сколько реально byte было использовано для записи или для чтения.
Давайте теперь реализуем асинхронную операцию readSum.
Как ее реализовать?
Вот readSum — это метод сокета, на вход получает буфер eSize, и вот у нас такой weightable.
Мы вначале засыпаем, и weightRadio возвращает false, зовется weightSuspend, и внутри weightSuspend мы прикапываем пару из
файлового дискриптора и карутин хэндла, куда-то внутрь нашего полера, который внутри дергает syscall select.
Ну, понятно, что там может быть не select, а может быть всё что угодно, пол е пол, как ю, юринг, всё что угодно.
В данном случае я selector рассматриваю.
И когда Select проснется, он позовет резьум на нашей коротине, вызовется В8-резьум.
И в этот момент мы сделаем read и вернем результат.
Вот так это работает.
И как это теперь реализовано?
Реализовано это таким образом.
Во-первых, у меня есть такая структурка Event.
которые представляют с собой три поля.
Во-первых, это файловый дискриптор, потом типа вента, читаем или пишем, и хэндл карутины.
И есть вот функция пол, которая мы в цикле постоянно дергаем.
И там просто дергается Pselect.
Перед Pselect мы инициализируем структурки ReadFDS, RideFDS, которые нужны для Pselect.
Дергаем Pselect, Pselect пробуждается, и опять в цикле проверяем
какие хендлы у нас готовы, какие готовы на тех просто зовем резьум.
Вот это можно сказать, что это такой простейший планировщик карутин и простейшая интеграция карутин с ивентлупом.
И как это используется из мейна, достаточно просто.
в мейне мы создаем этот полер, создаем таску и в бесконечном цикле дергаем функцию пол.
Как это работает?
На 11 строке вызывается таска, у меня карутина жадная, поэтому она пошла исполняться, дошла до пятой строки, до кой вейте заснула, и в этот момент сразу управление переходит назад в мейн и начался выполняться бесконечный цикл.
В бесконечном цикле
Селект когда-нибудь проснится в какой-то момент, позовет резьум, и карутина продолжит исполняться с пятой строки, после чего опять в какой-то момент заснет.
И так будет продолжаться до бесконечности.
То есть мы будем постоянно прыгать из этого поляр-пола в карутину и назад.
Теперь, что еще полезно иметь в асинхронном фреймворке?
Вот такую штуку.
Представьте, что вы пишете какой-то дистрибют к приложению, и один участник этого приложения через какие-то интервалы времени сообщает о своем присутствии, другому участнику приложения, отправляя сообщения.
Что для этого нужно сделать?
Для этого нужно просто поспать какое-то время, отправить сообщения.
Поспать, отправить сообщения и так далее.
Вот как это делается здесь, у меня в фреймворке.
Все очень просто, просто бесконечный цикл и бесконечном цикле делаем слип, который на самом деле является синхронным слипом.
Он усыпляет карутину на задное количество микросекунд.
Теперь как это интегрируется с полером?
Во-первых, как выглядит и weightable?
А weightable выглядит достаточно просто.
Мы в поляр просто прикапываем пару из таймпоинта и каротин-хендла.
Здесь все тривиально.
А с полером интегрируется это таким образом.
Мы заводим еще одну структуру.
который представляет собой пару из таймпоинта и хэндла.
И храним эти структурки в PriorityQ.
PriorityQ здесь элементы отсортированы у меня по возрастанию.
И Pselect, зовём с параметром TES, TimeStamp.
TimeStamp унициализируем
из самого первого таймера.
Если он есть, если нет, то выставляем какое-то там дефолтовое значение или ноль.
Вот поселект пробуждается, после пробуждения, ну вначале мы, понятно, работаем с сокетами, здесь это пропущено, так как не поместилось.
А потом смотрим, какие у нас таймеры пробудились.
Просто проходим по нашей прерыве Q,
И все, что готово для пробуждения, это то, что меньше now, now это текущий time stamp, то пробуждаем и зовем резьум.
Вот, все достаточно просто.
В принципе, вот то, что я рассказал, это уже достаточно для написания каких-то простейших сетевых приложений, например, ping'еров, потом echo client echo server и так далее.
Ну, давайте теперь попробуем написать что-нибудь более сложное.
Вот такой мотивирующий пример, допустим, мы пишем сетевое приложение, которое разбирает вот какой-то такой протокол.
Протокол состоит из месседжей, и месседжи выглядят вот так, это структурка с какими-то интовыми полями.
И у нас есть карутина протокол-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-х-
ритстракт может выглядеть вот, например, вот таким образом.
Здесь у меня вот обратите внимание, что то, что мы возвращаем, называется tfUchT.
И вот сейчас я покажу, как этот tfUchT реализовать.
Идея будет в том, что этот TFUCHET совмещает с собой тазк, про который я ранее рассказывал, и awaitable.
То есть на этом TFUCHET можно будет позвать кое-вейт и дождаться результата из карутины.
План реализации у меня такой.
Во-первых, у меня есть две карутины.
Первая карутина, та, которая дёргает другую карутину, называется колер, а та, которая выполняет работу, она называется коле.
И вот в этой коле-карутине, в таске, которая соответствует коле-карутины, мы реализуем все методы, необходимые для awaitable.
И коле-карутина должна будет пробуждать колер-карутину.
Когда это нужно делать?
У нас есть Final Suspend, про который я ранее говорил.
Final Suspend зовется после того, как мы позвали Coryton.
А пробудить корутину изначальную нужно как раз после Coryton.
Поэтому Final Suspend — это замечательное место, где можно пробудить первоначальную корутину.
И для этого, для пробуждения, мы напишем специальную политику.
Давайте вначале посмотрим, как реализована фьюча.
У нас есть опять же две сущности, которые карутини соответствуют.
Это фьюча, которая держит внутри себя карутин-хэндл и еще будет промес.
Я говорил, что карутин-хэндл — это не владеющая сущность.
Поэтому нужно карутин-хендл разрушать.
Но мы делаем так, чтобы эта фьюча владела карутин-хендлом, поэтому в Деструкторе фьючи разрушаем карутин-хендл.
Зачем мы это делаем?
У меня карутина после завершения работы остается в спящем состоянии, чтобы из нее можно было извлечь результат.
А если она
Остается в спящем состоянии, нужно ее разрушить руками, позвав Дестрой.
Вот, собственно, это делается в Деструкторе Фьюча.
Дальше, в промисе, который соответствует этому Фьючу,
Есть два поля.
Во-первых, это value, это optional, которое содержит результат.
И color.
Это handle-карутины, которая позвала кое-вейт.
Здесь у вейт-соспензе мы как раз кольера прикапываем внутрь промеса.
Давайте теперь посмотрим на промес, как он выглядит.
Он выглядит вот так.
Что здесь нужно отметить?
Во-первых, значение мы с этим в ретон-вэлью, который зовется при вызове ретон-вэлью, это нас-optional.
Еще колер мы инициализируем специальной штукой NOB-корутин.
Это корутин, который ничего не делает.
Это поставлено на всякий случай, чтобы нигде не делать проверки на NULL, чтобы у нас вот этот корутин-хэндл-колер был всегда валидный.
Давайте теперь посмотрим, как реализован Final Awaiter.
Это самая интересная часть.
Он реализован вот так.
И здесь используется VWate Suspend, используется третий вариант семантики VWate Suspender.
Здесь VWate Suspend мы возвращаем
вот этот самый колер, который когда-то прикопали.
И evadeSospend работает так, что если мы из него возвращаем CaroutineHandle, то этот CaroutineHandle пробуждается.
На нем зовется резьум неявным образом.
И вот эту фичу мы как раз здесь используем.
Что остается добавить?
Обработка исключений.
Здесь все достаточно просто.
мы в value переделываем с optional t на optional вариант.
В этом варианте лежит либо результат, либо exception.
И если у нас случился exception, мы в value с этим как exception.
И тогда VVT-резьум будет непросто.
Возврат value A нужно вот немножко вот так попреседать.
и попробовать вначале получить value, если получилось то возвратили, если не получилось то просто кинули исключение.
Давайте теперь с помощью этого всего реализуем более сложные штуки.
Вот часто, когда мы пишем сетевой, распределенный код, бывает такое, что мы
запустили несколько карутин, несколько операций, и нужно либо дождаться результата всех операций, потом что-то начать делать, либо дождаться выполнения какой-то одной конкретной операции, а про результаты всех остальных забыть, или вообще их отменить.
И вот это у меня называется ALL, это когда мы ждем всех, или ANE, когда мы ждем кого-то одного.
Вот ALL реализовать очень просто.
Вот пример использования на двенадцатой строке мы зовём All на двух карутинах.
И работает это так, мы просто в цикле на каждом фьючере зовём CoEweight и складываем всё в вектор, а потом его возвращаем.
То есть всё тривиально.
С Any дела обстоят гораздо хуже.
Эйни реализован таким нетривиальным образом.
Я здесь показываю реализацию для Future Void.
Если Эйни для Future T там немножко сложнее будет, это вам домашнее задание, как для Future T реализовать.
Для Void вот так.
Вначале мы проверяем,
Готовали какая-нибудь карутина.
Вдруг так оказалось, что кто-то уже завершился почему-то.
Если это так, то мы просто зовём на пятой строке коритон.
Если это не так, то нам нужно взять самого себя, то есть self.
и посетить в качестве колера всем карутином.
С этим мы с помощью прямого вызовы в ВЭД-соспент.
Мы зовём его ВЭД-соспент, туда передаём хэндл на самого себя, сетится колер, и когда какая-либо одна карутина проснулась, какая первая проснулась, она позвала Final Suspent, пробудила нас,
И мы пробудимся вот на десятой строке.
Вот после вот этого цикла, смотрите, у нас ожидание бесконечное suspend always.
Вот мы в этом suspend always будем висеть, пока кто-нибудь не завершится и не позовет final suspend.
После того, как позвали Final Suspend, мы проснулись.
На один строке позвали Coreytone.
Дальше у нас работают деструкторы.
И если какие-то FUCHI еще что-то делали где-то или должны что-то делать, они на самом деле отменятся и не будут уже нас пробуждать.
Теперь, как это всё тестировать?
Мы что-то напрограммировали какой-то сетевой код.
Хотелось бы его потрогать, например, в UnitTest.
Оказывается, что это делать очень легко в UnitTest.
Можно просто создавать лямбда карутины.
Таким образом, они создаются.
Вы просто пишете лямбду, как есть.
руками у нее указываете тип, какой у вас будет тип, например, здесь вот фьюча-войт.
Вот, и все, это будет лямбда-корутина.
Еще надо, понятное дело, запустить.
На шестой строчке запускается, как обычная лямба.
Дальше мы крутимся в цикле, пока корутина не закончилась, а потом можем что-то проверить.
Например, можем проверить, что слипс работал, что мы поспали, нужное количество секунд.
Точно так же можно сколь угодно сложные унитесты делать, например, тестировать какие-то клиент серверные приложения, клиента создать в одной лямбде, сервер создать в другой лямбде и запустить взаимодействие между ними.
Какие тут есть подводные камни?
Обычно в лямбда хочется через квадратные скобки делать капчуринг каких-то вльюсов.
И вот оказывается, оно все работает так, что после шестой строки у нас лямбда приостанавливается и все, что было закапчурено, оно все разрушается.
Оказывается, вот этот капчуринг не переносится автоматически в стейт карутины.
Поэтому, на самом деле, вот здесь вот на 7 строке будет useAutoFree, то есть A уже будет разрушена, и мы получим непонятно что.
Как-то лечить, вот если вы используете карутины в лямдах, то лучше копчуринг вообще никогда не делать, а явным образом все передавать через аргументы, в этом случае значения автоматически сохранятся в стейте карутины, и у вас будет все хорошо.
Давайте теперь посмотрим на результаты и на бенчмарке этой супер-библиотеки.
Ну, во-первых, я написал кучу разных хелперов, да, про них я здесь подробности не рассказываю.
У меня есть line reader, которая синхронно может по строчкам делить.
Есть bytewriter-byte-reader, которая гарантированно читает или пишет задненное количество byte.
И вот с помощью этого механизма я написал
уже такой более сложный эхоклиент.
Что касается бенчмарка, в качестве бенчмарка я взял библиотечку либо вент, классическая сишная библиотечка, она была записана, реализована больше 20 лет назад, чем она хороша.
В ней есть бенчмарк, который работает следующим образом.
Там создается n-пайпов.
Эти пайпы друг с другом обмениваются одним байтиком.
При этом у нас есть ограниченное число пайпов, которые реально делают свою работу, а все остальные просто висят внутри полера и ничего не делают, просто мешают им работать.
Плюс мы делаем там заденное количество райтов.
Я бенчмарк имплементировал на своей библиотеке.
То есть, полностью всю логику, как он есть, имплементировал то, что он работает так же, как в Либовенте.
Я просто убедился с помощью счетчиков.
Я поставил счетчики, которые считают вызовы ридов и вызовы райтов.
Убедился, что значение счетчиков из
Имплементация либо вента соответствует моей имплементации.
Значит, у меня работает точно так же.
Запустил, получил такие результаты.
Что мы здесь видим?
Здесь синие полоски.
Это три варианта либо вента с разными бекендами.
Это E-Poll, пол и Select.
Зеленые полоски — это моя библиотечка, Core.io, она называется.
Четыре бекенда, E-Poll, пол, Select и U-Ring.
Paxu — это количество дискрипторов, которые мы создавали по Y — это сколько микросекунд, работал бенчмарк.
В бенчмарке мы всегда делаем тысячи райтов по одному байтику.
Получается, что в целом моя библиотека работает не хуже, даже чуть лучше, чем либо вент, чем классический вариант.
В принципе, я считаю это успехом.
что я ещё реализовал на библиотеке.
Про это можно было отдельный доклад, на самом деле, сделать.
Я реализовал RouthServer и InmemoryDistributedKVLU Storage.
Вот это всё у меня работает.
Реализовал TLS-слой и асинхронный DNS-UDP-Resover, где всё можно скачать.
Во-первых, RAV Server и KVL Storage по первому репозиторию, core.io по второму.
Asistar, я привожу как пример хорошего карутина framework.
Причем этот framework, он написан был давно, и уже используется людьми в продакшене, и он в принципе идеологически похож на то, что делаю я.
Вот в качестве примера можно брать.
На этом всё.
Спасибо.
Большое спасибо.
Поехали по вопросам.
Давай сразу... Можешь вернуться на 12 слайд?
Так.
Вопрос такой.
Разве после выполнения evade suspend не нужно еще раз вызывать evade ready, чтобы убедиться, что можно возвращаться, а не снова спать?
После evade suspend evade ready... Нет, здесь ничего вызывать не нужно.
Здесь после evade suspend мы сразу возвращаем Boolean, true или false.
Если true,
то значит мы автоматически уснём, если false, то значит не уснём.
Но если вы делаете какую-то свою логику, то вы можете позвать Wait Ready, но здесь логика понятна, здесь у нас результат уже есть.
Результат Хэндл, мы его прикопали, значит результат есть.
Всё, и Wait Ready звать не нужно.
Отлично.
И сразу к слайду с замерами, с графиками.
Был вопрос, почему на 1024 дискрипторах все-либо одинаково?
Но это так кажется, они на самом деле немножко отличаются.
Это действительно какая-то очень странная анонсия.
Здесь на самом деле есть еще одна странная вещь, которую никто не заметил, 64.
На 64 есть некий такой всплеск, который потом падает.
И что самое интересное, я тестировал на разных операционках, Windows Linux MacOS и вот такой небольшой всплеск на 64 есть везде почему-то.
В общем, не знаю почему с этим хорошо бы разобраться, конечно.
Понятно.
Так, вопрос.
Можно ли и при suspend в крутине передать значение наверх или для этого использовать yield?
При использовании suspend имеется в виду weight suspender.
Вот где-нибудь вот здесь.
А какое значение наверх передать?
А ВЭД-соспент предполагает какие-то действия при засыпании карутины, да?
То есть мы здесь реализуем логику засыпания.
Здесь значения мы никакие не передаем.
Но теоретически, конечно, можно там позвать какой-нибудь резьум на каком-нибудь хендлере.
Но это бесполезно, потому что в ВЭД-соспент вы можете хендлер вернуть, тот, который вам надо резьумить.
Крутины выполняются параллельно, как в тредах, или только одна активная крутина может быть в один момент времени.
Крутины можно выполнять в тредах.
Если у вас есть какой-то трет-пул, и на этом трет-пуле вы выполняете крутины, у вас в один момент времени будет несколько активных крутин.
Если у вас один трет, то в один момент времени только одна активная крутина.
Вопрос.
Мы не могли вместо варианта использовать expector, чтобы ловить ошибки?
Конечно могли, но это стандарт 23, который почти никем не поддерживается.
Понятно.
Есть ли сравнительные тесты по загрузке процессора?
Таких тестов нет.
Вопрос очередной провокационный, когда появятся удобные крутины на основе трет-пула, как все шарфы, которые работают сразу из коробки?
Я предполагаю, что все плюс-плюс, скорее всего, такого никогда не появятся, потому что все любят
реализовать какие-то карутины удобные для самого себя, поэтому у нас есть миллион карутинных библиотек, которые реализуют теленые карутины со своим особым вкусом.
Какие есть рекомендации по бешевному плавному переводу имеющегося приложения с колбеков на карутины?
Но это вопрос, конечно, очень интересный.
Все зависит от самого приложения.
Надо смотреть на его, на его архитектуру.
Можно было бы просто, если приложение как-то внутри композировано по модулю, можно постепенно переписывать те или иные модули.
У вас есть какой-то event loop, который крутит эти callback.
Правильно?
И можно с этим event loop-ом интегрировать корутины и постепенно переписывать части приложения или новые части приложения реализовывать уже на корутинах.
Понятно.
На этом с вопросами у нас вроде бы как всё.
Ещё раз спасибо.
Это был Алексей Озерицкий.