co_await似乎不是最理想的?

时间:2017-07-25 18:56:02

标签: c++ c++-coroutine

我有一个异步功能

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_,只有在我们得到{{1}时才需要保留它们} coroutine_handle (顺便说一句,我不确定我是否可以在这里继续引用参数。)

如果包装函数可以直接将await_suspend(h)作为参数,那将会更有效。

这可能是一个隐含的论点:

coroutine_handle

或者它可能是一个特殊的关键字参数:

Promise f(coroutine_handle<> h);
co_await f();

我在这里遗漏了什么吗? (其他开销并不大。)

3 个答案:

答案 0 :(得分:3)

Coroutine TS定义的“协程”系统旨在处理异步功能:

  1. 返回类似未来的对象(表示延迟返回值的对象)。
  2. 类似未来的对象具有与延续功能相关联的能力。
  3. <g>不符合这些要求。它不会返回类似未来的对象;它通过延续函数“返回”一个值。这个延续作为参数传递,而不是你对对象的返回类型。

    async_foo发生时,生成未来的潜在异步进程预计已经启动。或者至少,co_await机器使可能使其开始。

    您提议的版本在co_await功能上失效,这是允许await_ready处理潜在异步进程的功能。在生成未来和调用co_await之间,该过程可能已完成。如果有,则无需安排恢复协程。因此,它应该发生在这个线程上。

    如果那个小筹码效率低下困扰你,那么你就必须像Coroutine TS想要的那样做事。

    处理此问题的一般方法是await_ready将直接执行coro_foo并返回类似于async_foo的类似机制的未来对象。您的问题是.then本身没有类似async_foo的机制,因此您必须创建一个。

    这意味着.then必须传递coro_foo一个存储async_foo的仿函数,一个可以通过未来的延续机制更新的仿函数。当然,您还需要同步原语。如果在执行仿函数之前已经初始化了句柄,那么仿函数会调用它,恢复协程。如果仿函数在没有恢复协程的情况下完成,仿函数将设置一个变量让await机器知道该值已准备就绪。

    由于句柄和此变量在await机器和仿函数之间共享,因此您需要确保两者之间的同步。这是一个相当复杂的事情,但它是任何coroutine_handle<>式的机器所需要的。

    或者你可以忍受轻微的低效率。

答案 1 :(得分:1)

如果我们使用类似未来的类,则可以直接从async_foo调用{p> 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;
}

看起来并不贵, 但它没有显着改善问题中的代码 (它也伤害了我的眼睛)

答案 2 :(得分:0)

当前的设计有一个重要的未来,co_await采用一般表达而不是调用表达。

这允许我们编写如下代码:

auto f = coro_1();
co_await coro_2();
co_await f;

我们可以并行运行两个或多个异步任务,然后等待它们。

因此,coro_1的实施应该在其调用中开始工作,而不是await_suspend

这也意味着应该有一个预先分配的内存,coro_1会将结果放在哪里,以及coroutine_handle的位置。

我们可以使用不可复制的Awaitable和保证的副本省略 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);
}