Comments 8
Как то не увидел из статьи, да и из кода простой идеи: можно взять отдельно какие то написанные корутины и просто использовать их.
А библиотека должна предоставить только операторы co await для своих типов или функции возвращающие awaiter
Например, насколько я понял в движке куча глобальных штук типа менеджера таймеров, таким образом чтобы подождать какое то время в корутине нужно только создать awaiter который в await suspend в глобального менеджера регистрирует по таймеру coroutine handle как калбек
И в await resume не делает ничего
Аналогично со всем остальным. А статья кажется предлагает писать свою корутину на каждый чих или даже делать эти операции частью логики корутины
Да, можно взять стороннюю либу -- кое-какие варианты в конце статьи приведены, но оно не совсем подходит под идею использования корутин в стиле тасков. При этом статья одновременно знакомит с подноготной сопрограмм. Почему бы и нет?
На каждый чих? В статье за awaiter выступает использование Future. Можно писать функции, которые возвращают Future и использовать в тасках с возвращаемым значением.
В плагине UE5Coro до момента написании статьи не было возможности использования co_return (т.е. не было нормальных тасков), а в реализации от EpicGames их таски предназначены для многопоточного исполнения без возможности их запуска в основном потоке.
Это говорит лишь о плохой реализации корутин. К сожалению многие компании/люди ещё не поняли как это должно выглядеть.(см. boost asio с очень странной и корявой реализацией)
Корутины практически никак не должны зависеть ни от задач которые исполняют, ни от исполнителя, ни от наличия/отсутствия многопоточности.
Для себя конечно использовал свои, но это мой личный эксперимент (https://github.com/kelbon/kelcoro)
Насчет future не понял посыла, ведь эффективнее и вероятно удобнее делать что-то такое:
struct time_awaiter {
std::chrono::milliseconds ms;
bool await_ready() const { return false; }
void await_suspend(std::coroutine_handle<> handle) const {
GetWorld().Timers().ExecuteAfter(ms, handle);
}
void await_resume() {}
};
Далее достаточно это вернуть из opeartor co_await для Delay или ещё какой-то функции(возможно функции менеджера таймеров сразу же)
И работать это будет с любой достаточно хорошо написанной корутиной(кроме чего-то типа однопоточного генератора, ведь в нём co_await с suspend это логическая ошибка)
Да по сути тот же Future, только в левой руке.
Future (тот же awaitable), на мой взгляд, даёт возможность инкапсулировать использование coroutine_handle
и ряда специальных методов:
По SetResult вызываем resume тем самым передавая выполнение обратно с подкидыванием возвращаемого значения в await_resume
. Функция, которая возвращает Future инициирует какое-либо отложенное действие, а затем, в колбеке (у меня это лямбда) выставляет результат. Ну как вариант.
Не позиционирую себя как человека, который точно знает как должно быть (технология относительно новая в C++ и, насколько слышал, спорная), но я тоже стараюсь обобщить на разные юзкейсы. Хотя тут есть некоторая привязка к фреймворку, так как использую UE делегаты, для того, чтобы аудитории понятнее было. В остальном, данная реализация даёт возможность писать асинхронный код используя только типы CoroTasks::TTask<>
и CoroTasks::TFuture<>
, а далее просто манипулировать ими. Ну и речь об однопоточной асинхронщине, так как её предоставляет сам движок (многопоточное вполнение он сам синкает в MainThrd), позволяя вешаться на неё пользовательскими колбеками.
Потому что C++20 дают не возможность написать готовую корутину, а возможность накидать себе готовый фреймворк для написания корутин. Эдакая сверх-абстракция. Может, в будущих стандартах докинут стандартных реализаций некоторых общих вещей (тот-же task, например)
В отличии от нового чудного механизма коротин, где всё прекрасно. В таком методе у вас есть полный контроль над происходящим и можно даже сериализовать состояние и продолжить после загрузки. Можно набирать очереди и выстраивать pipe-line-ы. Из плюсов можно даже без плюсов использовать.
Для удобства написания таких конечный автоматов можно использовать аналогию с трассами на которых расставлены контрольные точки. Каждый участок выполняется атомарно. Есть конечно некоторые оговорки, но если их соблюдать можно писать даже на C примерно так:
/* track-fn.h */
#pragma once
enum TrackConsts { track_no_limit=-1, track_start_line=0, track_end_line=-1 };
enum TrackResultCodes { track_rc_done=0, track_rc_active=1, track_rc_int=2 };
typedef struct Track { int line,limit; } Track;
#define TRACK_RESET(v) { Track *_track=(v); _track->line=0; _track->limit=1; }
#define TRACK_SET_LIMIT(track,n) { (track)->limit=n; }
#define TRACK_BEGIN(v) { Track *_track=(v); track_begin: \
switch(_track->line) { default: case 0: TRACK_POINT
#define TRACK_POINT { case __LINE__: _track->line=__LINE__; \
if (_track->limit>=0 && !_track->limit--) { _track->limit=1; return 1; } }
#define TRACK_END track_end: case -1: _track->line=-1; return 0; } \
track_interrupt: return 2; }
#define TRACK_END_R track_end: _track->line=-1; _track->limit=1; return 0; } \
track_interrupt: return 2; }
#define TRACK_REPEAT_LAST goto track_begin;
#define TRACK_LEAVE goto track_end;
#define TRACK_INTERRUPT goto track_interrupt;
#define TRACK_CALL(fn,state) { int rc; \
for(fn##_reset(state);0!=(rc=fn(state));) { \
if (rc==track_rc_int) { TRACK_INTERRUPT } else { TRACK_POINT } \
}}
/* tracks.c */
#include <stdio.h>
#include "track-fn.h"
typedef struct {
Track track[1];
int in0;
} fn1_state;
void fn1_reset(fn1_state *self) { TRACK_RESET(self->track); }
int fn1(fn1_state *self) {
TRACK_BEGIN(self->track)
printf("\tfn1.1\t%d\n",self->in0);
TRACK_POINT
printf("\tfn1.2\t%d\n",self->in0);
TRACK_END
}
typedef struct {
Track track[1];
fn1_state fn1[1];
int i;
} fn2_state;
void fn2_reset(fn2_state *self) { TRACK_RESET(self->track); }
int fn2(fn2_state *self) {
TRACK_BEGIN(self->track)
printf("fn2.1\n");
TRACK_POINT
for(self->i=1;self->i<=3;self->i++) {
self->fn1->in0=self->i; /* function input arg */
TRACK_CALL(fn1,self->fn1)
}
TRACK_POINT
printf("fn2.2\n");
TRACK_END
}
int main(int argc, char const *argv[]) {
fn2_state s[1];
fn2_reset(s);
s->track->limit=3; fn2(s);
printf("--\n");
s->track->limit=track_no_limit; fn2(s);
return 0;
}
"Почему бы не сделать один класс? Зачем специализация?"
Кстати, да, игрался с этим пол года назад - не пропускает если использовать std::enable_if<...> для выбора нужного метода. Концепты, вроде, раньше шаблонов резолвятся
С концептами тоже не выйдет. Даже если добавить функциям return_value
и return_void
requires, то при компиляции ругнётся: C3782 (обещание сопрограммы не может содержать return_value и return_void одновременно):
template<typename T>
concept CIsNotVoid = !TIsSame<T, void>::Value;
template<typename T>
concept CIsVoid = TIsSame<T, void>::Value;
template<typename ReturnType, typename TaskType>
struct TPromise_Base : FPromise_Exception
{
TMulticastDelegate<void(ReturnType)> OnDone;
template<typename T = ReturnType>
void return_value(T Result) requires CIsNotVoid<T>
{
if (CurrentException)
{
OnException.ExecuteIfBound(CurrentException);
OnException.Unbind();
}
else if (OnDone.IsBound())
OnDone.Broadcast(Result);
}
template<typename T = ReturnType>
void return_void() const requires CIsVoid<T>
{
if (CurrentException)
{
OnException.ExecuteIfBound(CurrentException);
OnException.Unbind();
}
else if (OnDone.IsBound())
OnDone.Broadcast();
}
};
Корутины в UnrealEngine