Как стать автором
Обновить

Комментарии 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<> и упоминать не о чем, то ли они эту проблему тоже не решили, но не признаются в этом :)

Хорошее решение этой задачи - shared_ptr. И вроде бы в cppcoro я его видел.

Ещё можно просто отменять корутину в деструкторе возвращаемого значения.

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), подключился пользователь (слушаете сокет серера), пришел запрос от пользователя (слушаете сокет пользователя), и т.д. - и прямо так сразу в лупе в этом потоке обрабатываете. В итоге у вас все будет работать в один поток, а дополнительные потоки запускать будете только для каких-то очень ресурсоемких медленных операций, если они у вас будут.

"больные ублюдки" - вот что я понял про с++

На всех других языках это пишется в пару строк

  1. Во многих других языках существует навороченный рантайм, который сам обеспечивает работу event loop и тасует таски по тредам без участия программиста. Там этот рантайм какой-то добрый дядя написал за вас. В C++ такого рантайма нет (и было бы очень странно, если бы его туда добавили), поэтому его функционал мы пилим сами, об этом и статья.

  2. Как уже было сказано, в C++20 поддержка корутин только очень низкоуровневая, поэтому оно все так страшно выглядит. В следущих стандартах должны завозить более высокоуровневые примитивы поверх этого, которые будут проще в использовании (в C++23 уже появился std::generator, например). Ну или можно прямо сегодня взять тот же cppcoro где они уже есть, и писать корутины без странных танцев.

А в целом да, согласен. Мы в мире C++ с юности привыкли к боли и унижениям, нас такое не пугает.

На C++ тоже можно взять folly и написать в пару строк. Корутины в C++ действительно переусложнённые, подход из Rust мне нравится больше, но абсолютно везде сделать поддержку ввода-вывода и минимально необходимых функций для корутин займёт немало кода.

Мы в мире 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-ы или нет.

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

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории