По большому счету он здесь не нужен, особенно в constexpr контексте. В целом избыточно, и уже давно игнорируется компиляторами — inlining решается оптимизатором. Если только как запасной парашют для ODR violation
Полностью согласен, покрывать и поддерживать весь sql - жизни не хватит. Статья о возможностях constexpr, на полноценный легковесный валидатор всего и вся надеяться не приходится. Сложность проверок и их количество ограничены адекватностью подхода, пока потенциальная польза перевешивает его недостатки.
Как минимум, потому что у него есть еще одна полезная функция.
Ссылки, переданные в func, остаются действительными до тех пор, пока возвращаемый отправитель не завершит работу, после чего переменные выйдут из области видимости.
Если приходится запускать асинхронные задачи, которые требуют собственного состояния — например, буфера или соединений, но мы хотим гарантировать, что оно живёт, пока задача выполняется, то нужен let_value, ниже покажу, какие проблемы создает then:
Невозможно передать напрямую в spawn, ожидается один уровень вложенности sender'a и главное: buf может быть уничтожен до завершения read_from_socket, т.к. then не знает, что надо продлить его жизнь.
А let_value гарантирует, что buf жив до конца всей цепочки.
Без let_valueмы имеем нестабильный lifetime данных, ручное распутывание sender<sender<U>> и во многих случаях довольно сложно написать что-то читаемое.
Кстати, scope из примера - он же async_scope - API из предложения P3149, который используется совместно с P2300
позволяет запускать множество асинхронных операций через .spawn(sender);
отслеживает их завершение;
предоставляет способ дождаться окончания всех операций (через .join()).
execution::async_scope scope;
scope.spawn(
ex::let_value(ex::just(), [] {
return ex::just(); // Любой sender
})
);
co_await scope.join(); // Ждём завершения всех задач
Действительно, путаница вышла, я поправлю описание. Спасибо.
then действительно всегда создаёт новый sender, заворачивая в него результат вызова, но с той разницей от let_value, что последний не будет плодить sender'ы, и если возвращаемый тип уже является sender'ом - он его пробросит напрямую:
Да, можно встроить несколько независимых веток обработки ошибок в один pipeline, как в вашем примере. И да, вы можете адаптировать std::expected к модели senders/receivers, но для этого потребуется свой адаптер или комбинация существующих (let_value, just_error, variant_sender).
Первый способ - преобразовать expected в sender, второй способ - танцевать с let_error. Но не факт, что получится без танцев с бубном
Эти библиотеки были и ранее, если поискать, думаю, можно много найти.
Из того, что выходит по первым кликам: Серверные примеры от NVIDIA, Библиотека Intel для микроконтроллеров, реализующая частичную поддержку P2300, + обсуждения intel о интеграции sender/receiver.
Если наткнетесь на интересные примеры - присылайте, будет, что разобрать
Да, всё именно так, лямбда создаётся в момент выполнения tag_invoke, то есть припостроении sender'а ex::schedule(ps), а исполнение лямбды происходит внутри пула, как часть start(os)-операции, в примере - при вызове sync_wait(pipeline). Поэтому корректнее будет сказать, что измеряется задержка между моментом построения sender-а и моментом фактического запуска работы в пуле.
Видимо, слишком размытой получилась формулировка:
<...> выводит задержку переводa задачи в пул <...>, иначе говоря,замеряет время от вызова schedule(...) до реального начала выполнения задачи в пуле.
Речь идет о замере времени с момента описания/конструирования, до фактического старта.
Да, все именно так. Если в процессе вызова Emit(), когда мьютекс уже захвачен, один из коллбэков вызовет код, который приводит к уничтожению подписки, то возникнет дедлок
int main()
{
Observable observable;
// Создаем подписку, чей коллбэк вызывает отписку (симулируем удаление подписки)
observable.Subscribe(
[ &observable ]()
{
// Так как emit() уже захватил мьютекс, попытка захватить его внутри отписки приведет к дедлоку
{
auto tmp = observable.Subscribe( []() {} );
}
} );
// Вызовем emit(), который вызовет наш коллбэк
observable.Emit();
// Любой код после не будет выполнен
}
Можно решить эту проблему через применение std::recursive_mutex, или копирование списка коллбэков под защитой мьютекса с последующим их вызовом вне критической секции.
Но если говорить об этом дальше, то стандартный std::mutex не учитывает и асинхронное переключение контекста, и может привести к ситуациям, когда корутина приостанавливается, удерживая блокировку, образуя дедлок. В таком случае нам уже надо смотреть в сторону чего-то по типу бустовых файбер-мьютексов, async_mutex-ов и т.д.
Если вы используете этот код...
Благо, не использую его вообще нигде, разве что для примеров в статьях. Спасибо, это очень дельное замечание, о котором мне, наверное, стоило упомянуть в статье.
По большому счету он здесь не нужен, особенно в constexpr контексте. В целом избыточно, и уже давно игнорируется компиляторами — inlining решается оптимизатором. Если только как запасной парашют для ODR violation
Полностью согласен, покрывать и поддерживать весь sql - жизни не хватит. Статья о возможностях constexpr, на полноценный легковесный валидатор всего и вся надеяться не приходится. Сложность проверок и их количество ограничены адекватностью подхода, пока потенциальная польза перевешивает его недостатки.
Так и есть, в большей части это вопрос правильной работы в параллелизме.
Те же Senders/Receivers в C++26 тоже не больший ад, чем просто новый инструмент.
Инструменты нового механизма и синтаксис как-то интуитивно понятнее на самом простом.
Простыни кода и все нюансы Senders/Receivers в рамках одной статьи упихнуть точно невозможно, но от простого к сложному, шаг за шагом
Соберу обратную связь, дополню второй статьей, где будет больше реальных примеров.
Пока что идейно, но по мере дополнений будет и практическая сторона данной темы.
По случайности отправил комментарий не в эту ветку.
Как минимум, потому что у него есть еще одна полезная функция.
Если приходится запускать асинхронные задачи, которые требуют собственного состояния — например, буфера или соединений, но мы хотим гарантировать, что оно живёт, пока задача выполняется, то нужен
let_value
, ниже покажу, какие проблемы создаетthen
:Невозможно передать напрямую в
spawn,
ожидается один уровень вложенности sender'a и главное:buf
может быть уничтожен до завершенияread_from_socket
, т.к.then
не знает, что надо продлить его жизнь.А
let_value
гарантирует, чтоbuf
жив до конца всей цепочки.Без
let_value
мы имеем нестабильный lifetime данных, ручное распутываниеsender<sender<U>>
и во многих случаях довольно сложно написать что-то читаемое.Кстати,
scope
из примера - он жеasync_scope
- API из предложения P3149, который используется совместно с P2300позволяет запускать множество асинхронных операций через
.spawn(sender)
;отслеживает их завершение;
предоставляет способ дождаться окончания всех операций (через
.join()
).Действительно, путаница вышла, я поправлю описание. Спасибо.
then
действительно всегда создаёт новыйsender
, заворачивая в него результат вызова, но с той разницей отlet_value
, что последний не будет плодить sender'ы, и если возвращаемый тип уже является sender'ом - он его пробросит напрямую:Поэтому аккумулировать через then результат прошлых вызовов
then/let_value
, напрямую пробрасывая sender'ы без распаковки - чревато слоями-sender'ов.А, вот,
let_value
вернет честныйsender
в одной плоскости -> sender<int>Да, можно встроить несколько независимых веток обработки ошибок в один pipeline, как в вашем примере. И да, вы можете адаптировать
std::expected
к модели senders/receivers, но для этого потребуется свой адаптер или комбинация существующих (let_value
,just_error
,variant_sender
).Первый способ - преобразовать
expected
в sender, второй способ - танцевать с let_error. Но не факт, что получится без танцев с бубномЭти библиотеки были и ранее, если поискать, думаю, можно много найти.
Из того, что выходит по первым кликам: Серверные примеры от NVIDIA, Библиотека Intel для микроконтроллеров, реализующая частичную поддержку P2300, + обсуждения intel о интеграции sender/receiver.
Если наткнетесь на интересные примеры - присылайте, будет, что разобрать
Да, всё именно так, лямбда создаётся в момент выполнения
tag_invoke
, то есть при построении sender'аex::schedule(ps)
, а исполнение лямбды происходит внутри пула, как частьstart(os)
-операции, в примере - при вызовеsync_wait(pipeline)
. Поэтому корректнее будет сказать, что измеряется задержка между моментом построения sender-а и моментом фактического запуска работы в пуле.Видимо, слишком размытой получилась формулировка:
Речь идет о замере времени с момента описания/конструирования, до фактического старта.
Да, все именно так. Если в процессе вызова Emit(), когда мьютекс уже захвачен, один из коллбэков вызовет код, который приводит к уничтожению подписки, то возникнет дедлок
Можно решить эту проблему через применение std::recursive_mutex, или копирование списка коллбэков под защитой мьютекса с последующим их вызовом вне критической секции.
Но если говорить об этом дальше, то стандартный std::mutex не учитывает и асинхронное переключение контекста, и может привести к ситуациям, когда корутина приостанавливается, удерживая блокировку, образуя дедлок. В таком случае нам уже надо смотреть в сторону чего-то по типу бустовых файбер-мьютексов, async_mutex-ов и т.д.
Благо, не использую его вообще нигде, разве что для примеров в статьях. Спасибо, это очень дельное замечание, о котором мне, наверное, стоило упомянуть в статье.