co_await кажется неоптимальным?

У меня есть асинхронная функция

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);

Я что-то здесь упускаю? (Другое, что накладные расходы не такие большие.)

6

Решение

Система «сопрограмм», определяемая Coroutine TS, предназначена для обработки асинхронных функций, которые:

  1. Возврат объекта, подобного будущему (объект, представляющий отложенное возвращаемое значение).
  2. Подобный будущему объект обладает способностью связываться с функцией продолжения.

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Стилистика машин требует.

Или вы могли бы просто жить с незначительной неэффективностью.

3

Другие решения

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;
}

Это не выглядит очень дорого,
но это не значительно улучшает код в вопросе.
(Это также вредит моим глазам)

1

Текущий дизайн имеет важное будущее, которое 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);
}
0
По вопросам рекламы ammmcru@yandex.ru
Adblock
detector