【问题标题】:co_await appears to be suboptimal?co_await 似乎不是最理想的?
【发布时间】:2017-12-31 20:43:17
【问题描述】:

我有一个异步函数

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_,只需要保留它们直到我们在await_suspend(h) 中获得coroutine_handle
(顺便说一句,我不确定我是否可以在此处保留对参数的引用。)

如果包装函数可以直接获取coroutine_handle作为参数,效率会高很多。

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

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

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

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

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

【问题讨论】:

  • 我的库,concurrencpp,确实以非常优化的方式很好地处理了期货和协程,请查看:github.com/David-Haim/concurrencpp
  • @DavidHaim 以及如何将它与asio::async_readWSARecv 等现有API 一起使用?顺便说一句,你可能想更新它,initial_suspend 不能返回 bool。
  • 您必须编写一个包装器,将concurrencpp::promise 传递给任何boost::async_xxx 并返回concurencpp::future。然后你使用普通的co_await

标签: c++ c++-coroutine


【解决方案1】:

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

  1. 返回类似未来的对象(表示延迟返回值的对象)。
  2. future-like 对象能够与延续函数相关联。

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&lt;&gt; 的函子,该函子可以由未来的延续机制更新。当然,您还需要同步原语。如果句柄在仿函数执行时已经初始化,则仿函数调用它,恢复协程。如果仿函数在没有恢复协程的情况下完成,仿函数将设置一个变量让等待机器知道该值已准备好。

由于句柄和此变量在 await 机器和仿函数之间共享,因此您需要确保两者之间的同步。这是一件相当复杂的事情,但这是.then 风格的机器所需要的。

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

【讨论】:

  • await_ready 用于同步结果(例如缓存结果或无效参数错误)。如果并行线程在auto e = f();e.await_ready(); 之间完成异步任务,则出现问题。
  • @Abyx: "然后出了点问题" 这是线程代码; “出错”可以很简单,比如“你的线程意外休眠了一段时间,而另一个进程真的很短”。功能性线程系统必须能够处理所有此类可能性。不能保证快速前进,并且不得假设。而co_await 就是为了能够处理这种情况而设计的。
  • 许多现有的 API 使用回调。 (包括每个 C API,包括 OS API)。为什么要坚持建立在这些基于回调的原语之上并带有额外同步的期货?
  • @Abyx:也许你注意到协程的返回类型是某种形式的“未来”,协程机制包括在内部为协程函数创建一个“承诺”,用于通信到那个“未来”的返回值。 Promise/future 范式遍布 Coroutines TS 设计;这就是它的意义所在。该范式允许多个级别的协程和延续每个级别都显式地调用函数。
  • 所以我想将 HTTP 服务器重写为协程,但可惜它使用 epoll,显然我需要让 Linux 开发人员为我编写另一个与协程兼容并使用相同的 API性能,因为我想要带有协程的服务器,而不是慢 10% 的带有协程的服务器。
【解决方案2】:

当前设计具有重要的未来,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);
}

【讨论】:

  • 看起来像堆损坏,因为从 coro_func 返回后,在 lambda 中(由此)捕获的 Awaitable 对象被破坏。可能需要使用 enable_ptr_from_this...但是除了这个小问题,这很好。
  • 只有一个Awaitable,没有副本或移动。 Awaitable 甚至包含一个atomic,以强调这一点。
【解决方案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;
}

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

【讨论】:

  • "它也伤我的眼睛" 那别看它;)我有点认真。将 Future&lt;T&gt; 实现塞入某个标题中,并在需要时使用它。 co_await 的优点在于它使异步代码出现同步。当您实际使用 co_awaitcoro_foo 时(这是您 99% 的时间会看到的),您的代码将具有明显的同步操作逻辑,而实际上是异步的。
猜你喜欢
  • 2019-07-31
  • 1970-01-01
  • 2019-03-09
  • 1970-01-01
  • 2013-01-19
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多