У меня есть асинхронная функция
void async_foo(A& a, B& b, C&c, function<void(X&, Y&)> callback);
Я хочу использовать его в сопрограмме без стека, поэтому я пишу
auto coro_foo(A& a, B& b, C& c, X& x) /* -> Y */ {
struct Awaitable {
bool await_ready() const noexcept { return false; }
bool await_suspend(coroutine_handle<> h) {
async_foo(*a_, *b_, *c_, [this, h](X& x, Y& y){
*x_ = std::move(x);
y_ = std::move(y);
h.resume();
});
}
Y await_resume() {
return std::move(y);
}
A* a_; B* b_; C* c_; X* x_; Y y_;
};
return Awaitable{&a, &b, &c, &x};
}
тогда я могу использовать это так:
Y y = co_await coro_foo(a, b, c, x);
и компилятор переписал бы это так:
auto e = coro_foo(a, b, c, x);
if (!e.await_ready()) {
<suspend>
if (e.await_suspend(h)) return;
resume-point:
<resume>
}
Y y = e.await_resume();
С этим сопрограмма будет держать a_
, b_
, а также c_
когда это приостановлено, когда это только должно держать их, пока мы не получим coroutine_handle
в await_suspend(h)
,
(Кстати, я не уверен, смогу ли я сохранить ссылки на аргументы здесь.)
Было бы гораздо эффективнее, если бы функция-обертка могла напрямую получить coroutine_handle
в качестве аргумента.
Это может быть неявный аргумент:
Promise f(coroutine_handle<> h);
co_await f();
Или это может быть специальный ключевой аргумент:
Promise f(coroutine_handle<> h);
f(co_await);
Я что-то здесь упускаю? (Другое, что накладные расходы не такие большие.)
Система «сопрограмм», определяемая Coroutine TS, предназначена для обработки асинхронных функций, которые:
async_foo
не соответствует этим требованиям. Он не возвращает объект, подобный будущему; он «возвращает» значение через функцию продолжения. И это продолжение передается как параметр, а не как то, что вы делаете с типом возвращаемого объекта.
К тому времени co_await
случается, потенциально асинхронный процесс, который породил будущее, должен уже иметь началось. Или, по крайней мере, co_await
машины делают это возможный чтобы это началось.
Ваша предложенная версия проигрывает на await_ready
особенность, которая позволяет co_await
обрабатывать потенциально асинхронные процессы. Между временем и будущим await_ready
называется, процесс, возможно, закончился. Если это так, нет необходимости планировать возобновление сопрограммы. Поэтому это должно произойти прямо здесь, в этой теме.
Если эта незначительная неэффективность стека беспокоит вас, то вам придется действовать так, как того требует Coroutine TS.
Общий способ справиться с этим coro_foo
будет непосредственно выполнять async_foo
и вернуть похожий на будущее объект с .then
механизм Ваша проблема в том, что async_foo
сам по себе не имеет .then
механизм, поэтому вы должны создать его.
Это означает coro_foo
должен пройти async_foo
функтор, который хранит coroutine_handle<>
тот, который может быть обновлен механизмом продолжения будущего. Конечно, вам также понадобятся примитивы синхронизации. Если дескриптор был инициализирован ко времени выполнения функтора, то функтор вызывает его, возобновляя сопрограмму. Если функтор завершает работу без возобновления сопрограммы, он устанавливает переменную, чтобы механизм ожидания узнал, что значение готово.
Поскольку дескриптор и эта переменная являются общими для механизма ожидания и функтора, вам необходимо обеспечить синхронизацию между ними. Это довольно сложная вещь, но это все .then
Стилистика машин требует.
Или вы могли бы просто жить с незначительной неэффективностью.
async_foo
может быть вызван непосредственно из coro_foo
если мы используем будущий класс.
Это будет стоить нам одного распределения и атомарной переменной:
static char done = 0;
template<typename T>
struct Future {
T t_;
std::atomic<void*> addr_;
template<typename X>
void SetResult(X&& r) {
t_ = std::move(r);
void* h = addr_.exchange(&done);
if (h) std::experimental::coroutine_handle<>::from_address(h).resume();
}
bool await_ready() const noexcept { return false; }
bool await_suspend(std::experimental::coroutine_handle<> h) noexcept {
return addr_.exchange(h.address()) != &done;
}
auto await_resume() noexcept {
auto t = std::move(t_);
delete this; // unsafe, will be leaked on h.destroy()
return t;
}
};
Future<Y>& coro_foo(A& a, B& b, C& c, X& x) {
auto* p = new Future<Y>;
async_foo(a, b, c, [p, &x](X& x_, Y& y_) {
x = std::move(x_);
p->SetResult(y_);
});
return *p;
}
Это не выглядит очень дорого,
но это не значительно улучшает код в вопросе.
(Это также вредит моим глазам)
Текущий дизайн имеет важное будущее, которое co_await
принимает общее выражение, а не выражение вызова.
Это позволяет нам писать код так:
auto f = coro_1();
co_await coro_2();
co_await f;
Мы можем запустить две или более асинхронных задач параллельно, а затем ждать их обеих.
Следовательно, реализация coro_1
следует начинать свою работу по вызову, а не по await_suspend
,
Это также означает, что должна быть предварительно выделенная память, где coro_1
положил бы его результат, и где бы он взял coroutine_handle
,
Мы можем использовать не копируемые Awaitable
и гарантированная копия elision.
async_foo
будет вызван из конструктора Awaitable
:
auto coro_foo(A& a, B& b, C& c, X& x) /* -> Y */ {
struct Awaitable {
Awaitable(A& a, B& b, C& c, X& x) : x_(x) {
async_foo(a, b, c, [this](X& x, Y& y){
*x_ = std::move(x);
y_ = &y;
if (done_.exchange(true)) {
h.resume(); // Coroutine resumes inside of resume()
}
});
}
bool await_ready() const noexcept {
return done_;
}
bool await_suspend(coroutine_handle<> h) {
h_ = h;
return !done_.exchange(true);
}
Y await_resume() {
return std::move(*y_);
}
atomic<bool> done_;
coroutine_handle<> h_;
X* x_;
Y* y_;
};
return Awaitable(a, b, c, &x);
}