Comments 34
Благодарю за статью. Как по мне — самая лучшая вводная по корутинам. Поправь только в тексте про await_ready(), указывается дважды false как возвращаемый результат.
Теперь вот вопрос по Task. А не надо ли его сохранять? Там хэндл корутины же лежит. А что будет, если он будет уничтожен, а мы вызовем resume? А то кажется, что doSomething работает только потому, что память незакораптилась.
Спасибо!
Если речь про первый пример, где у нас Task ничего не делает, то там вроде все безопасно. Возвращаемое значение нигде и никак не используется, .resume() мы делаем внутри await_suspend, куда coroutine_handle передается по значению, сам Awaitable живет во фрейме корутины, а фрейм корутины и ее промис гарантированно будут жить до полного выполнения этой корутины. Я в блоге Рэймонда Чена видел примеры для fire-and-forget корутин, они там примерно так же делают. Прогнал на всякий случай такой пример с AddressSanitizer из clang, он молчит. Добавил calloc(), чтобы явно перезаписать стек после создания корутины — ничего не сломалось.
А если вы про ту навороченную версию с поддержкой вложенных корутин, что в конце статьи - то да, вы совершенно правы, и в той статье ее автор об этом тоже пишет, что возвращаемый хендл должен жить пока выполняются операции, но у него нет хорошего решения этой задачи. Кстати интересно, в той же cppcoro их возвращаемый task<> самой top-level корутины во всех примерах оборачивается в cppcoro::sync_wait(), а про его лайфтайм ничего не сказано. То ли у них нет такой проблемы с task<> и упоминать не о чем, то ли они эту проблему тоже не решили, но не признаются в этом :)
var client = await server.accept(); // принимаем входящее подключение
var request = await socket.read(...));
Похоже код никого не интересует. А код зачем-то приведен с подлянкой!
Куда делся client,
и зачем он вообще нужен, если он дальше не используется и откуда взялся socket
кто-нибудь может объяснить?
socket = client
это конечно самая простая версия, но только это не может быть опечаткой. Поэтому возникает вопрос: может автора код не интересует? Тогда не имеет смысла дальше анализировать страшные классы приведенные в статье.
Да всё просто же, писался код одновременно со статьёй, копировался кусками, периодически редактировался и снова кусками переносился. Вот и исправление socket на client полностью не перенеслось.
Исправить это, конечно же, надо - но вот далеко идущие выводы про "автора код не интересует" я бы делать не стал.
Вот и исправление socket на client полностью не перенеслось.
это как это? Когда могло произойти такое исправление в трех строчках, в пяти?
не верю.
Вы не верите в то, что человек по невнимательности мог совершить банальную ошибку? Серьезно?
Сначала назвал переменную "client", через какое-то время после вычитки решил исправить на "socket", пошел снизу вверх, исправил в трех строчках, а четвертую пропустил по невнимательности, потому что что-то отвлекло. А потом копипастом эта ошибка перенеслась во второй пример.
У вас ни раз в жизни за всю карьеру программиста не было, что выполняя рутинные действия, когда нужно сделать множество однообразных изменений во многих местах, вы случайно пропускали одно из них, о чем потом узнавали по подсветке в IDE или по ошибке компилятора? Теперь уже моя очередь сказать "не верю", ну разве что только если вы не Чак Норрис.
И да, напомню, то, до чего вы докопались - это вообще псевдокод, его нельзя ни проверить линтером, ни запустить перед публикацией. А понять при чтении, где именно ошибка и какое должно быть правильное название — настолько просто и очевидно, что обсасывать это три комментария подряд явно не стоит, как уже сказали выше, правилом хорошего тона в таких случаях является написать личное сообщение автору о найденной ошибке.
что выполняя рутинные действия, когда нужно сделать множество однообразных изменений во многих местах
Как мне показалось мы говорили о 5-ти строчках которые определяют-задают смысл всей статьи, и в этих строчках допущена ошибка, именно это меня и расстроило. Я не понимаю при чем здесь "множество однообразных изменений во многих местах", я про 5-ть строчек говорю, а вы про что?
Но честно говоря даже общий смысл статьи по моему ускальзает, потому что:
"WTF? Серьезно? Почему ТАК сложно? Зачем столько танцев с бубном чтобы запустить простейшие корутины?!"
Начать с того что я бы не стал так безапеляционно совмещать слова "корутины" и "простейшие". Вы сами показали что все действительно очень сложно, а главное совсем не понятно, что же вы сравнивали на самом деле.
Кроме модного слова "корутины" и некоторого непонятного шаманства вокруг него в статье нет ничего полезного для "чайника", которому она предназначена, судя по названию. ИМХО.
Мне просто интересно на чем основано восхищение тех кто вашу статью плюсует, поэтому я и указал на ошибку публично. Раз с поддержкой у вас проблем нет, я думаю критика вам не помешает, а то как бы корона не вросла и не повредила что-то в голове.
Начать с того что я бы не стал так безапеляционно совмещать слова "корутины" и "простейшие".
"Простейшие" - это про логику, для которых они используются. Как в примере, сделали запрос, вывели результат, сделали второй запрос, вывели результат. Всего 4 строчки кода, простейший линейный алгоритм. Но заставить "под капотом" эти 4 строчки работать - сложно.
модного слова "корутины"
Модного? Идеи корутин были подробного описаны еще в 1980-х, а по факту были еще даже в языке Simula из 60-х. Boost.Coroutine больше десятка лет, асинкам в C#/Python/JS больше десятка лет, библиотеке protothreads - 20 лет. Это просто в C++ очень долго доезжают вещи, которые были "модными" десять-пятнадцать лет назад, а сегодня являются индустриальным стандартом.
в статье нет ничего полезного для "чайника", которому она предназначена, судя по названию.
Ну вот есть человек, знакомый с C++, но при этом полный чайник в теме реализации корутин на C++, и который хочет узнать, что это такое и как их использовать. Статья именно для него, с разбором с самых основ и примерами реализации. Если это по-вашему совершенно бесполезно, то мне очень интересно послушать, а какой же должна быть "полезная" с вашей точки зрения статья на эту тему.
Можно просто нажать Ctrl+Enter, чтобы сообщить автору об опечатке
Разве select и poll -- это асинхронный ввод-вывод, а не неблокирующий? Я не линухоид, поэтому и спрашиваю. Просто неблокирующий к асинхронщине отношения не имеет и толку с него в этом плане примерно нуль (смысл асинхронного ввода-вывода, как правильно указано в статье, -- сообщать о завершении операции ввода-вывода, ну а неблокирующий лишь говорит о том, что файл готов к дальнейшей работе -- а, скажем, настоящий честный дисковый файл всегда готов).
В линухе для неблокирующего i/o как раз poll и select не нужны, неблокирующим может быть и обычный read(), если на дескриптор выставить нужный ioctl.
Из "классических" API попадающих именно под определение асинхронности выше, модно назвать, например, aio - там как раз так, запустили операцию и сказали, как нас уведомить о ее завершении. Но эти уведомления там или неудобные (posix-сигнал процессу), или неэффективные (коллбэк из нового потока), поэтому оно не особо популярно. Зато довольно часто берут poll/epoll (которые, если вдуматься, как раз блокирующие), делают на их основе event loop, и получают именно что асинхронный API с коллбэками.
так и не понял почему метод с корутинами отличается от метода рассматриваемого в начале статьи и где говорится, что можно создать потоки для задания... По сути оно одно и тоже получается, только меньше кода самописного...
Разница в том, что с корутинами асинхронный код выглядит проще и понятнее, как синхронный - не надо городить коллбэки или дробить алгоритм на кучу функций.
Спасибо за статью, она действительно закрыла некоторые пробелы в понимании но если честно с момента рассказа Гор Нишанова про корутины когда их ещё только пропихивали в стандарт, году в 18 на одном из открытых мероприятий яндекса, и посмотрев уже не один доклад по корутинам на cpp russia и cppcon, я пока ментально видимо не готов к корутинам и не могу принять то чем они проще. На "бумаге" все выглядит вроде не плохо, а потом начинается десяток всяких мета классов для того чтоб это работало с не очевидным и не до конца понятными переходами между состояниями и когда начинаю задумываться, а что если где то что то пойдёт не так и возникнет какая то гонка то я умру это дебажить. Для меня код из "старых" плюсов выглядит максимально просто и понятно, я понимаю примерные места где может быть гонка и мне понятно как это отлаживать в случае проблем, а вот корутины мне уже начинают boost asio напоминать и вспоминая дни отладки там бросает в дрож..
На "бумаге" все выглядит вроде не плохо, а потом начинается десяток всяких мета классов для того чтоб это работало с не очевидным и не до конца понятными переходами между состояниями и когда начинаю задумываться, а что если где то что то пойдёт не так и возникнет какая то гонка то я умру это дебажить.
Тут соглашусь, все упирается в развитость классов стандартной библиотеки и поддержку со стороны отладчиков.
В C#, например, где корутины в виде async/await используются уже десяток лет, их использовать очень легко именно благодаря тому, что вся внутренняя кухня скрыта за этими async/await и Task, а event loop и менеджер потоков реализован в самом рантайме .Net, то есть разработчику ни о чем из этого думать не надо. Плюс поддержка со стороны отладчика, чтобы видеть в трейсах привычные и понятные функции, а не что-то страшное, что из них нагенерировал компилятор.
Что-то я не совсем понимаю или совсем не понимаю. doSomething() вызывает две функции с co_await .... Я так понимаю пока одна не закончится следующая не будет вызвана. А где же асинхронность? Можно какой-нибудь пример где два request выполняются наперегонки и печатают результат когда он будет доступен.
Можно какой-нибудь пример где два request выполняются наперегонки и печатают результат когда он будет доступен.
Один похожий пример есть в статье, под спойлеров про await_ready == true. Там нужно использовать модифицированный Awaitable, который запускает операциею еще в своем конструкторе, и потом можно сделать
auto req1 = client.performRequestAsync("https://postman-echo.com/get");
auto req2 = client.performRequestAsync("http://httpbin.org/user-agent");
co_await req1;
co_await req2;
- запросы начнут выполняться еще в момент performRequestAsync() параллельно, а потом в co_await мы просто дождемся, когда они все будут выполнены.
Если же надо и выводить результат сразу же по завершению, то можно взять навороченный Task из конца статьи, и разбить doSomething на две вложенных корутины:
Task<void> getAndPrint(WebClient& client, std::string url)
{
auto result = co_await perfromRequestAsync(client, url);
std::cout << "Ready: " << result.code << " - " result.data << std::endl;
}
Task<void> doSomething(WebClient& client)
{
auto task1 = getAndPrint("http://url1");
auto task2 = getAndPrint("http://url2");
co_await task1;
co_await tasl2;
}
Спасибо. Получается что вся кухня с callback и select никуда не делась, а работает себе в отдельном потоке, а в основном потоке что-то типа красивой обёртки - пользовательский интерфейс где можно выполнять высокоуровневые операции. Преимущество как мне кажется, что количество callback в коде будет значительно меньше. В данном случае будет только один :) Но если нужно добавить что-то другое помимо curl, то нужен либо отдельный поток, либо добавить это в WebClient::runLoop(). Я правильно понимаю? Например асинхронное чтения файла или server.accept(). Как бы вы поступили для такого расширения этого кода?
Получается что вся кухня с callback и select никуда не делась, а работает себе в отдельном потоке, а в основном потоке что-то типа красивой обёртки
Нет, не получается. Тот поток, который исполняет цикл событий - это и есть основной.
Но если нужно добавить что-то другое помимо curl, то нужен либо отдельный поток, либо добавить это в WebClient::runLoop()
В целом, да. Для каждого цикла обработки событий нужен свой поток (а лучше набор потоков по числу ядер).
Хотя конкретно в случае асинхронных сокетов и асинхронных http-запросов проще отказаться от curl и использовать только сокеты.
правильно написал @mayorovp
Хотя конкретно в случае асинхронных сокетов и асинхронных http-запросов проще отказаться от curl и использовать только сокеты.
в интерфейсах к сокетам есть все что нужно для ассинхронности, приплетать там какие-то "корутины" для которых даже нет однозначного определения (если вы попробуете посмотреть кто что понимает под этим названием) да еще и с непонятной внешней библиотекой в стабильности которой еще надо убедиться, совершенно не к чему.
У меня когда-то было решение как раз такой задачки с которой начал автор, она решается с использованием двух потоков достаточно просто, если вы умеете писать решения с асинхронностью на потоках, а если не умеете, то корутины вам точно не помогут.
Может быть я как-нибудь напишу статью про это решение, раз уж мне напомнили задачку и теперь она у меня снова перед глазами как 17 лет назад.
да еще и с непонятной внешней библиотекой в стабильности которой еще надо убедиться
Это CURL-то непонятная библиотека с неизвестной стабильностью? Лол. Она массово используется уже больше 20 лет, в настоящий момент, по грубым прикидкам, работает более чем на 20 миллиардах девайсов по всему миру, от серверов, десктопов и телефонов до embedded-систем и дронов на Марсе (это сейчас не шутка, а реальный факт). Плюс присутствует и используется практически в любом Linux-дистрибутиве и в современных версиях Windows. Непонятная и нестабильная библиотека, ну да. Она свою стабильность и полезность уже доказала неоднократно. Вы, наверное, и OpenSSL считаете "непонятной и нестабильной библиотекой"?
И кстати, CURL - это не замена сокетам. CURL - это реализация HTTP/HTTPS/QUIC/FTP/иещемноговсего клиента. Без него и других библиотек с одними только голыми сокетами для выполнения той же задачи (https-запросы) вам придется городить вручную свой огромный велосипед.
CURL был выбран именно потому, что мы в примере пишем веб-клиент, а libcurl - это самая популярная и надежная библиотека для этого. Если бы стояла задача делать не GET-запросы, а просто писать и читать в/из сокетов, то мы бы сделали такой же event loop с голыми сокетами, например, на базе poll или epoll, который является стандартным механизмом ядра Linux и используется, например, в nginx и libevent.
Получается что вся кухня с callback и select никуда не делась, а работает себе в отдельном потоке, а в основном потоке что-то типа красивой обёртки - пользовательский интерфейс где можно выполнять высокоуровневые операции.
Не обязательно, можно обойтись даже одним-единственным потоком. В основном потоке вы запускаете event loop, а дальше он уже реагирует на события (пользовательский ввод, входящие подключения, таймеры, и т.д.), которые в том же потоке и обрабатываются.
Но если нужно добавить что-то другое помимо curl, то нужен либо отдельный поток, либо добавить это в WebClient::runLoop().
Зависит от того, что именно мы делаем. Например, если нам в описанный выше пример с CURL надо просто добавить пару своих внешних событий, то curl_multi_poll может принимать список дополнительных файловых дескрипторов для проверки, и будет так же реагировать на события в них, нужно только дописать обработчик.
Если же нам нужен CURL, но он только один из элементов паззла, то мы можем построить event loop на базе классических poll/epoll, и добавлять в них fd из CURL (там есть метод, чтобы получить список используемых дескрипторов), и мониторить их в poll/epoll наравне со всеми остальными сокетами и дескрипторами.
Ну и, само собой, можно обойтись вообще без CURL, если он не нужен :)
Вроде понятно. Я просто раньше думал что можно обойтись и без второго потока. Каким-то образом жонглировать в единственном потоке всеми состояниями и переключать контекст по мере того как становятся доступными данные. А внутри под капотом select/epoll + файловые дескрипторы ну или CURL. Такое возможно? Скажем вызываешь co_await а там начинает шуршать вся эта машина состояний и возможно выполнится какой-то другой co_await из другого контекста и там пойдёт выполнение дальше...
Да, да, такое возможно, все как вы сказали. В основном потоке вы запускаете event loop, он сидит и слушает события. Произошло событие - пользователь нажал кнопку (если вы слушаете дескриптор stdin), подключился пользователь (слушаете сокет серера), пришел запрос от пользователя (слушаете сокет пользователя), и т.д. - и прямо так сразу в лупе в этом потоке обрабатываете. В итоге у вас все будет работать в один поток, а дополнительные потоки запускать будете только для каких-то очень ресурсоемких медленных операций, если они у вас будут.
"больные ублюдки" - вот что я понял про с++
На всех других языках это пишется в пару строк
Во многих других языках существует навороченный рантайм, который сам обеспечивает работу event loop и тасует таски по тредам без участия программиста. Там этот рантайм какой-то добрый дядя написал за вас. В C++ такого рантайма нет (и было бы очень странно, если бы его туда добавили), поэтому его функционал мы пилим сами, об этом и статья.
Как уже было сказано, в C++20 поддержка корутин только очень низкоуровневая, поэтому оно все так страшно выглядит. В следущих стандартах должны завозить более высокоуровневые примитивы поверх этого, которые будут проще в использовании (в C++23 уже появился std::generator, например). Ну или можно прямо сегодня взять тот же cppcoro где они уже есть, и писать корутины без странных танцев.
А в целом да, согласен. Мы в мире C++ с юности привыкли к боли и унижениям, нас такое не пугает.
Мы в мире C++ с юности привыкли к боли и унижениям
в каком-то грустном мире вы живете. Мы в нашем С++ мире привыкли к неограниченным возможностям, правда это было еще до 2011 года. Видимо это с 2011 года вас загоняют в мир боли и унижений. Это печально.
Мы в нашем С++ мире привыкли к неограниченным возможностям, правда это было еще до 2011 года.
Одно другому не мешает. Сила C++ действительно в его неограниченных возможностях, за это он активно используем и любим. Вот только с большой силой приходит и большая ответственность, ну, разве что если только разработчик не страдает синдромом Даннинга-Крюгера.
А что до 2011 года, как человек, начавший иметь дело с плюсами в начале 2000-х, могу констатировать факт, что C++98/С++03 и C++11 - сильно разные языки, и C++11 просто несопоставимо лучше в плане безопасности, удобства и приятности разработки. На старых версиях плюсов до 11 года я разрабатывать бы взялся только за тройную доплату за вредность.
вряд ли вам поможет C++11 или даже C++115, когда вы не понимаете, что так писать нельзя:
var socket = await server.accept(); // принимаем входящее подключение
var request = await socket.read(...));
var result = await db_сient.do_query(transform_request(request));
await socket.write(transform_result(result));
socket.close();
Компилятор сделает особую магию.
Вы заблокировали сервер, никакая магия вам не поможет! В течении времени работы с полученным сокетом, сервер не принимает другие входящие сокеты, не важно есть у вас там await
-ы или нет.
Вряд ли вам поможет не блокировать потоки, если вы не понимаете, что такое псевдокод, который вообще не обязательно должен быть целиком и полностью корректным, а всего лишь служит для демонстрации общей идеи, ровно как эскиз на салфетке отличается от чертежа по ескд. А общая идея там демонстрируется не в первой строке, а дальше. Я переписал эту строку в примерах, надеюсь вы теперь сможете жить и спать спокойно.
Корутины C++ для чайников: пишем асинхронный веб-клиент