【问题标题】:Is this a reasonable use case for coroutines?这是协程的合理用例吗?
【发布时间】:2021-03-04 19:33:02
【问题描述】:

我试图了解协程的用例,我想知道这是否是 C++20 协程的合理用例。

我正在编写一个库来处理 UTF-8 字符流中的文本替换。我在想我会有以下类的方法:

std::u8string parse(std::u8string input_string);
std::u8string flush();

在对\parse 的调用结束时,一个替换可能处于未完成状态,因此,例如,如果有一个替换,例如,--- 到 — 那么一系列调用

auto a = charsub.parse(u8"and --");
auto b = charsub.parse(u8"- ");
auto c = charsub.parse(u8"--");
auto d = charsub.flush();

abcd的值分别初始化为“and”、“—”、“”和“--”。

我通过协程实现这个 API 有什么收获吗?如果是这样,代码会是什么样子?

【问题讨论】:

  • 你当前的实现是什么样的?
  • 协同例程适用于处理可能无限但您不需要预先进行所有处理的情况。即,您对部分结果进行尽可能多的处理,然后退出(不丢失状态)并允许应用程序继续处理部分结果,直到它需要更多。 A 稍微做作(但说明了这一点):计算所有素数的函数。你称之为获得第一个素数。尽可能多地处理,然后再次调用它以获得下一个素数等。
  • 不同之处在于它允许您将状态存储在单独的堆栈中(而不是对象(存储额外状态的常规方式))。如果将状态存储在堆栈中是有用的,那么协程可能是要走的路,否则可能会很昂贵(您需要为堆栈分配一块内存和一些空间来存储寄存器的状态(这不应该就这么多))。
  • @appleapple 还没有实现。它将有一个 FSM 来管理将存在于 charsub 对象中的替换。从早期的评论来看,这似乎不是协程的好案例。
  • 如果您有并行工作要做,协程很有用。您没有指定您的要求如何要求并行性。

标签: c++ c++-coroutine


【解决方案1】:

你的直觉是正确的用协程解决这个问题。文本转换和解析问题是协程的经典应用。事实上,Conway 用于第一个协程的示例与您的问题非常相似。

每当有一个函数需要跨调用保持状态时,就会想到协程。在 C++ 协程之前,我们可以使用函子或捕获 lambda 来解决此类问题。协程带来的不仅是维护本地数据的状态,还有维护本地逻辑状态的能力。

因此,与在每次调用时检查其入口状态的普通函数相比,协程代码可以更简单并且具有更好的流程。

除了在本地保持状态之外,协程还允许函数提供自定义点或挂钩,类似于作为回调传入的 lambda。

使用 lambdas 作为回调是控制反转的一个例子。然而,基于协程的设计与控制反转相反——“控制反转”(如果您愿意的话)返回到客户端代码,该代码决定何时使用co_await 以及如何处理结果。

例如,下面的协程可以生成单个字符或连续 3 个破折号,而不是产生转换后的字符串 - 然后应用程序代码可以用 em 破折号或其他东西替换 3 个破折号。这产生的“事件”可能是其他字符序列或模式的出现。协程的工作将缩小到扫描输入字符串 - 替换和转换将是另一个协程的责任

我对您的问题的协程解决方案相对简单,可能看起来与非协程函数没有太大区别。主要区别在于它在输入中保持破折号的状态,否则需要在外部保留。

它使用最小且直接的 C++ 协程机制——除了一件事。由于您的 parse 函数在每次调用时都会接受新输入,因此我需要以某种方式更新协程本地的输入字符串。这并不简单,可以通过不同的方式完成。

我选择在对本地字符串变量的引用上创建协程co_await。局部变量的引用/地址存储在协程承诺中。这是通过使用协程承诺的await_transform 方法拦截co_await 来实现的。

一旦promise有了局部变量的地址,就可以通过公共返回对象进行更新。

协程中的局部字符串变量是避免不必要的字符串复制的指针。

这种技术在访问协程中的局部变量时有点笨拙——虽然需要更多代码让协程改为 co_await 另一个会返回新输入字符串的协程,但这会更好。

我也避免使用u8string,因为它使用起来很痛苦

代码使用 gcc 11.2 和 vc++ 2022 版本 17.1 测试

g++ -std=gnu++23 -ggdb3 -O0 -Wall -Werror -Wextra -fcoroutines -o parsedashes parsedashes.cpp
$ parsedashes < <(echo -e "and --\n- \n--")

and
—

--

完整的程序

// parsedashes.cpp

#include <stdio.h>
#include <iostream>
#include <coroutine>
#include <string>

using namespace std;

static void usage()
{
  cout << "usage: parsedashes <in.txt" << "\n";
}

协程返回对象及其公共API

struct ReplaceDashes {
  struct Promise;
  using promise_type = Promise;
  coroutine_handle<Promise> coro;

  ReplaceDashes(coroutine_handle<Promise> h): coro(h) {}

  ~ReplaceDashes() {
    if(coro)
      coro.destroy();
  }

// resume the suspended coroutine
  bool next() {
    coro.resume();
    return !coro.done();
  }

// return the value yielded by coroutine
  string value() const {
    return coro.promise().output;
  }

// set the input string and run coroutine
  ReplaceDashes& operator()(string* input) {
    *coro.promise().input = input;
    coro.resume();
    return *this;
  }

它的内部承诺对象

  struct Promise {
// address of a pointer to the input string
    string** input;
// the transformed output aka yielded value of the coroutine
    string output;

    ReplaceDashes get_return_object() {
      return ReplaceDashes{coroutine_handle<Promise>::from_promise(*this)};
    }

// run coroutine immediately to first co_await
    suspend_never initial_suspend() noexcept {
      return {};
    }

// set yielded value to return
    suspend_always yield_value(string value) {
      output = value;
      return {};
    }
// set returned value to return
    void return_value(string value) {
      output = value;
    }

    suspend_always final_suspend() noexcept {
      return {};
    }

    void unhandled_exception() noexcept {}
// intercept co_await on the address of the local variable in
// the coroutine that points to the input string
    suspend_always await_transform(string** localInput) {
      input = localInput;
      return {};
    }

  };

};

实际的协程函数

ReplaceDashes replaceDashes()
{
  string dashes;
  string outstr;

// input is a pointer to a string instead of a string
// this way input string can be changed cheaply
  string* input{};

// pass a reference to local input string to keep in coroutine promise
// this way input string can be set from outside coroutine
  co_await &input;

  for(unsigned i = 0;;) {
    char chr = (*input)[i++];
// string is consumed, return the transformed string
// or any leftover dashes if this was the final input
    if(chr == '\0') {
      if(i == 1) {
        co_return dashes;
      }
      co_yield outstr;
// resume to process new input string
      i = 0;
      outstr.clear();
      continue;
    }
// append non-dash after any accumulated dashes
    if(chr != '-') {
      outstr += dashes;
      outstr += chr;
      dashes.clear();
      continue;
    }
// accumulate dashes
    if(dashes.length() < 2) {
      dashes += chr;
      continue;
    }
// replace 3 dashes in a row
// unicode em dash u+2014 '—' is utf8 e2 80 94
    outstr += "\xe2\x80\x94";
    dashes.clear();
  }

}

解析器 API

struct Charsub {

  ReplaceDashes replacer = replaceDashes();

  string parse(string& input) {
    return replacer(&input).value();
  }

  string flush() {
    replacer.next();
    return replacer.value();
  }

};

驱动程序

int main(int argc, char* argv[])
{
  (void)argv;

  if(argc > 1) {
    usage();
    return 1;
  }

  Charsub charsub;

  for(string line; getline(cin, line);) {
    cout << charsub.parse(line) << "\n";
  }
  cout << charsub.flush();

}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-02-13
    • 2019-11-14
    • 1970-01-01
    • 2019-05-30
    • 1970-01-01
    • 2019-03-10
    相关资源
    最近更新 更多