Рантайм оперирует рутинами со следующим интерфейсом:
template <class Runtime>
struct IRoutine {
virtual void Step(Runtime* rt) = 0;
// Метод может использоваться для уклонения от structured concurrency.
// С поддержкой этого метода есть проблемы, связанные с отсутствием
// связи между состоянием корутины и сконструированными/удаленными объектами.
// virtual void Discard() = 0;
};Функциональность рантайма описывается следующими концептами:
// После `Submit` когда-нибудь выполнит `Step` на `routine`
template <class Runtime>
concept Executor = requires(Runtime* rt, IRoutine<Runtime>* routine) {
{ rt->Submit(routine) } -> std::same_as<void>;
};
// После наступления времени `tp` выполнит `Step` на `routine`
template <class Runtime>
concept TimersManager =
requires(Runtime* rt, IRoutine<Runtime>* routine, TimePoint tp) {
{ rt->After(tp, routine) } -> std::same_as<void>;
};
enum class InterestKind : uint8_t {
None = 0b00,
Readable = 0b01,
Writable = 0b10,
};
// Когда файловый дескриптор `fd` станет доступен на чтение/запись,
// выполнит `Step` на `routine`
template <class Runtime>
concept IOManager = requires(Runtime* rt, IRoutine<Runtime>* routine, RawFd fd,
InterestKind ik) {
{ rt->WhenReady(fd, ik, routine) } -> std::same_as<void>;
// Детали реализации (epoll)
{ rt->Register(fd) } -> std::same_as<void>;
{ rt->Deregister(fd) } -> std::same_as<void>;
};Создание и запуск рантайма выглядит следующим образом:
EventLoop loop{/* num_workers= */2};
loop.Start(); // Альтернативное название - PinAndStart
// Do work
loop.Stop(); // Альтернативное название - StopAndJoinСтековый фрейм корутины описывается пользователем в виде структуры:
struct SteppingCoro {
std::optional<int> Step() {
switch (state) {
case 0:
for (; i < 3; ++i) {
state = 1;
return std::nullopt;
case 1:
}
return 3;
default:
std::abort();
}
}
size_t i = 0;
uint16_t state = 0;
};Для того, чтобы корутина могла запланировать своё пробуждение, в Step передаётся
контекст:
template <class Self, class Runtime>
struct Context {
Self* self;
Runtime* rt;
};
struct SleepingCoro {
template <class Self, class Runtime>
std::optional<Unit> Step(Context<Self, Runtime>* ctx) {
switch (state) {
case 0:
state = 1;
ctx->rt->After(Clock::now() + std::chrono::seconds{1}, ctx->self);
return std::nullopt;
case 1:
return Unit{};
default:
std::abort();
}
}
size_t i = 0;
uint16_t state = 0;
};Если использовать нездоровое количество макросов, получается что-то похожее на stackless-корутины:
struct SleepYield : Pc {
PROTO_CORO(Unit) {
PC_BEGIN;
SLEEP_FOR(100ms);
YIELD;
return Unit{};
PC_END;
}
};
struct CallsTwice : Pc {
PROTO_CORO(Unit) {
PC_BEGIN;
CALL_DISCARD(SleepYield{});
CALL(auto result, SleepYield{});
return result;
PC_END;
}
CALLS(SleepYield);
};В IRoutine корутина оборачивается с помощью адаптера:
template <class T, class Runtime>
struct Spawn final : IRoutine<Runtime> {
Spawn(T&& routine, In<Runtime>) : inner(std::move(routine)) {
}
void Step(Runtime* rt) override {
Context ctx{this, rt};
inner.Step(&ctx);
}
private:
T inner;
};Благодаря тому, что в функции Step корутины известен конкретный тип Runtime,
при использовании неподдерживаемого функционала возникает ошибка компиляции.
Так как реализация корутины довольно громоздкая, для последовательной композиции
корутин могут использоваться функциональные комбинаторы fmap и >>=
в форме FMap и AndThen, которые поддерживают pipe-based синтаксис пример.
Для связывания корутин в рантайме с обычным кодом используются блокирующие примитивы синхронизации:
Возможно, явно упоминаться в задаче они не будут в пользу функции вроде
template<class Coro, class Runtime>
decltype(auto) RunBlocking(Coro&& coro, Runtime& rt);
Yeah, but your scientists were so
preoccupied with whether or not they could,
they didn't stop to think if they should.
Благодаря тому, что IRoutine знает конкретный тип Runtime, можно реализовать
кастомизацию структуры IRoutine под рантайм, в котором она исполняется:
template <class Runtime>
struct IRoutine : Runtime::RoutineAux {
virtual void Step(Runtime* rt) = 0;
};В частности, RoutineAux может быть узлом интрузивной структуры данных.
На который я пока не решился.
template <class T>
concept Coroutine = requires {
typename T::Output;
// ...
};
struct DummyCoro : Pc {
PROTO_CORO(Unit) {
// ...
}
};
template <class Runtime>
concept Executor = requires(Runtime* rt) {
Coroutine<typename Runtime::YieldCoro>;
{ rt->Yield() } -> std::same_as<typename Runtime::YieldCoro>;
// TODO: SpawnedCoro<DummyCoro>/SpawnedCoro<typename DummyCoro::Output>?
Coroutine<typename Runtime::SpawnedCoro>;
{ rt->Spawn(DummyCoro{}) } -> std::same_as<typename Runtime::SpawnedCoro>;
};
template <class Runtime>
concept TimersManager = requires(Runtime* rt, TimePoint tp) {
Coroutine<typename Runtime::SleepUntilCoro>;
{ rt->SleepUntil(tp) } -> std::same_as<typename Runtime::SleepUntilCoro>;
};
template <class Runtime>
concept IOManager = requires(Runtime* rt) {
typename Runtime::FileDescriptor;
// 50 функций для работы с файловыми дескрипторами
};Плюсы альтернативного интерфейса:
- Содержит меньше деталей реализации (рантайм может предоставить произвольную логику ожидания, например, на таймере)
IOManagerсовместим с io_uring/ioringapi- Позволяет ограничивать доступный вызываемым корутинам io-функционал рантайма
Минусы:
- Более сложная реализация
IOManager - Типы всех корутин становятся зависимыми от типа рантайма