1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

c++20 の coroutine 使ってみる

Last updated at Posted at 2023-06-21

vc2022 で実験。

実験コードはこちら。
https://github.com/ousttrue/co_sample

最小限

とりあえず compile を通して必要最小限を調べる

#include <coroutine>
#include <functional>
#include <iostream>

// Promise
struct Co {
  struct promise_type // 👈重要
  {
    auto get_return_object() {
      return Co{std::coroutine_handle<promise_type>::from_promise(*this)}; // 👈重要
    }
    auto initial_suspend() {
      std::cout << "[promise_type] initial_suspend" << std::endl;
      return std::suspend_always{};
    }
    auto final_suspend() noexcept {
      std::cout << "[promise_type] final_suspend" << std::endl;
      return std::suspend_always{};
    }
    void unhandled_exception() { std::terminate(); }
    void return_void() {}
  };
  std::coroutine_handle<promise_type> handle; // 👈重要
};

// coroutine
Co run() {
  std::cout << "[run] before" << std::endl;
  co_return;
  std::cout << "[run] after" << std::endl;
}

int main(int argc, char **argv) {
  auto co = run();
  std::cout << "[main]before resume: " << co.handle.done() << std::endl;
  co.handle.resume(); // 👈重要
  std::cout << "[main]after resume: " << co.handle.done() << std::endl;
  return 0;
}

実行すると

$ ./prefix/bin/simple.exe
[promise_type] initial_suspend
[main]before resume: 0
[run] before
[promise_type] final_suspend
[main]after resume: 1

というふうになります。

resume() で進行する必要あり。
C#lua と同じ感じ。

ビルドに /std:c++latest が必要。
/std:C++20 でもよいかもしれない。

c++ の coroutine

関数 body で co_return, co_await, co_yield のいずれかを使う関数。
その返り値 R 型は、R::promise_type 型を持っている必要がある。

struct R
{
  struct promise_type
  {
    // 省略
  };
};
R run() {
  co_return; // co_return などが返り値の R::primise_type を要求する
}

要求を満たしていないと普通に関数がないコンパイルエラーになる。

promise_type

5つの member 関数が必要。

struct R {
  struct promise_type {
    auto get_return_object();
    auto initial_suspend();
    auto final_suspend() noexcept;
    void unhandled_exception();
    void return_void();
  };
};

要求を満たしていないと普通に関数がないコンパイルエラーになる。

std::coroutine_handle

コルーチンを再開(resume())したり、コルーチンが終了済み(done())か調べたりする。

R のメンバーに入れて get_return_object で初期化するのがパターン。

struct R {
  struct promise_type
  {
    auto get_return_object() {
      return R{
        .handle = std::coroutine_handle<promise_type>::from_promise(*this)
      };
    }
    // 他は省略
  };
  std::coroutine_handle<promise_type> handle; // 👈 これ
};

R のメンバーにしないと resume() する手段が無い?

await (suspend) してみよう

#include <coroutine>
#include <functional>
#include <iostream>
#include <thread>

struct Awaiter
{
  bool await_ready()
  {
    std::cout << "[Awaiter] await_ready" << std::endl;
    // false ならば suspend する。
    return false;
  }
  void await_suspend(std::coroutine_handle<> handle)
  {
    std::cout << "[Awaiter] await_suspend" << std::endl;
  }
  void await_resume() { std::cout << "[Awaiter] await_resume" << std::endl; }
};

// Promise
struct Co
{
  struct promise_type
  {
    auto get_return_object()
    {
      return Co{ std::coroutine_handle<promise_type>::from_promise(*this) };
    }
    auto initial_suspend()
    {
      std::cout << "[promise_type] initial_suspend" << std::endl;
      return std::suspend_always{};
    }
    auto final_suspend() noexcept
    {
      std::cout << "[promise_type] final_suspend" << std::endl;
      return std::suspend_always{};
    }
    void unhandled_exception() { std::terminate(); }
    void return_void()
    {
      std::cout << "[promise_type] return_void" << std::endl;
    }
  };
  std::coroutine_handle<promise_type> handle;
};

// coroutine
Co
run()
{
  std::cout << "[run] before" << std::endl;
  co_await Awaiter{};
  std::cout << "[run] after" << std::endl;
  // co_return;
}

int
main(int argc, char** argv)
{
  auto co = run();
  for (int i = 0; !co.handle.done(); ++i) {
    std::cout << "[main]resume " << i << std::endl;
    co.handle.resume();
  }
  return 0;
}
[promise_type] initial_suspend
[main]resume 0
[run] before
[Awaiter] await_ready
[Awaiter] await_suspend
[main]resume 1
[Awaiter] await_resume
[run] after
[promise_type] return_void
[promise_type] final_suspend

Awaiter をユーザーから隠して間接的に生成させる

上記の例では下記のように直接 Awaiter を作りました。

  co_await Awaiter{};

本来の使い方では、Awaiter を間接的に生成して隠蔽するデザインになっているようです。
2通りの手段があります。

  • Awaiter operator co_await(const T &)
  • Awaiter promise_type::await_transform(const T &)

Awaiter と Awaitable の違いが不明瞭。
operator co_await が Awaiter で、
await_transform が Awaitable と言っているようにみえた。

時間から Awaiter を生成する例。

co_await 10ms;
// Awaiter に変換する

Awaiter operator co_await(const chrono::milliseconds &)

// もしくは

Awaiter promise_type::await_transform(const chrono::milliseconds &)
    auto initial_suspend() {
      std::cout << "[promise_type] initial_suspend" << std::endl;
      return std::suspend_always{}; // 👈 awaiter
    }
    auto final_suspend() noexcept {
      std::cout << "[promise_type] final_suspend" << std::endl;
      return std::suspend_always{}; // 👈 awaiter
    }

https://cpprefjp.github.io/reference/coroutine/suspend_always.html

co_yield: 中断(suspend)するときに coroutine から呼び出し側に値を返す

#include <coroutine>
#include <functional>
#include <iostream>
#include <thread>

struct Awaiter
{
  bool await_ready()
  {
    std::cout << "[Awaiter] await_ready" << std::endl;
    return false;
  }
  void await_suspend(std::coroutine_handle<> handle)
  {
    std::cout << "[Awaiter] await_suspend" << std::endl;
  }
  void await_resume() { std::cout << "[Awaiter] await_resume" << std::endl; }
};

// Promise
struct Co
{
  struct promise_type
  {
    auto get_return_object()
    {
      return Co{ std::coroutine_handle<promise_type>::from_promise(*this) };
    }
    auto initial_suspend()
    {
      std::cout << "[promise_type] initial_suspend" << std::endl;
      return std::suspend_always{};
    }
    auto final_suspend() noexcept
    {
      std::cout << "[promise_type] final_suspend" << std::endl;
      return std::suspend_always{};
    }
    void unhandled_exception() { std::terminate(); }
    void return_void()
    {
      std::cout << "[promise_type] return_void" << std::endl;
    }

    int m_value = -1;
#if 1
    std::suspend_always yield_value(int value) // 👈 co_yield 時に呼ばれる
    {
      m_value = value;
      return {};
    }
#else
    std::suspend_always await_transform(int value) // co_await で書けばこう?
    {
      m_value = value;
      return {};
    }
#endif
  };
  std::coroutine_handle<promise_type> handle;
};

// coroutine
Co
run()
{
  std::cout << "[run] before" << std::endl;
  co_yield 1;
  co_yield 2;
  co_yield 3;
  std::cout << "[run] after" << std::endl;
  // co_return;
}

int
main(int argc, char** argv)
{
  auto co = run();
  for (int i = 0; !co.handle.done(); ++i) {
    std::cout << "[main]resume " << i << "=>" << co.handle.promise().m_value // 👈 m_value が更新される
              << std::endl;
    co.handle.resume();
  }
  return 0;
}
[promise_type] initial_suspend
[main]resume 0=>-1
[run] before
[main]resume 1=>1
[main]resume 2=>2
[main]resume 3=>3
[run] after
[promise_type] return_void
[promise_type] final_suspend

co_return で値を返す

#include <coroutine>
#include <functional>
#include <iostream>
#include <thread>

// Promise
template<typename T>
struct Co
{
  struct promise_type
  {
    auto get_return_object()
    {
      return Co{ std::coroutine_handle<promise_type>::from_promise(*this) };
    }
    auto initial_suspend()
    {
      std::cout << "[promise_type] initial_suspend" << std::endl;
      return std::suspend_always{};
    }
    auto final_suspend() noexcept
    {
      std::cout << "[promise_type] final_suspend" << std::endl;
      return std::suspend_always{};
    }
    void unhandled_exception() { std::terminate(); }

    T m_value = -1;
    void return_value(T value)
    {
      m_value = value;
      std::cout << "[promise_type] return_value" << std::endl;
    }
  };
  std::coroutine_handle<promise_type> handle;

  T Value()
  {
    return handle.promise().m_value;
  }
};

// coroutine
Co<int>
run()
{
  std::cout << "[run] before" << std::endl;
  co_return 345;
  std::cout << "[run] after" << std::endl;
}

int
main(int argc, char** argv)
{
  auto co = run();
  std::cout << "[main]before resume: " << co.handle.done() << " => "
            << co.Value() << std::endl;
  co.handle.resume();
  std::cout << "[main]after resume: " << co.handle.done() << " => "
            << co.Value() << std::endl;
  return 0;
}
[promise_type] initial_suspend
[main]before resume: 0 => -1
[run] before
[promise_type] return_value
[promise_type] final_suspend
[main]after resume: 1 => 345

再開(resume)するとき coroutine に値を渡す

Task ぽくするには、suspend 時に thread に処理を投げて、
thread 終了時に結果を resume に投入して再開する必要がありそう。

#include <chrono>
#include <coroutine>
#include <functional>
#include <future>
#include <iostream>
#include <thread>

// Promise
template<typename T>
struct Co
{
  struct promise_type
  {
    auto get_return_object()
    {
      return Co{ std::coroutine_handle<promise_type>::from_promise(*this) };
    }
    auto initial_suspend()
    {
      std::cout << "[promise_type] initial_suspend" << std::endl;
      return std::suspend_always{};
    }
    auto final_suspend() noexcept
    {
      std::cout << "[promise_type] final_suspend" << std::endl;
      return std::suspend_always{};
    }
    void unhandled_exception() { std::terminate(); }

    auto await_transform(const std::function<T()>& work)
    {
      struct awaiter
      {
        std::future<T> m_future;
        std::thread m_t;
        bool await_ready() { return false; }
        void await_suspend(std::coroutine_handle<>) {}
        T await_resume() // 👈 await_resume の返り値が co_await  の左辺値になる
        {
          std::cout << "thread block ? tid#" << std::this_thread::get_id()
                    << std::endl;
          return m_future.get();
        }
      };

      std::promise<T> p;
      std::future<T> f = p.get_future();
      std::thread t([work, p = std::move(p)]() mutable {
        std::cout << "[work]tid#" << std::this_thread::get_id() << std::endl;
        T value = work();   // 👈 thread 上で work を実行
        p.set_value(value); // 結果を返す
      });

      // join しないのでエラーにならなように
      t.detach();

      return awaiter{
        std::move(f),
        std::move(t),
      };
    }

    T m_value = -1;
    void return_value(T value)
    {
      m_value = value;
      std::cout << "[promise_type] return_value" << std::endl;
    }
  };
  std::coroutine_handle<promise_type> handle;

  T Value() { return handle.promise().m_value; }
};

// coroutine
Co<int>
run()
{
  std::cout << "[run] before" << std::endl;
  int result = co_await []() {
    std::this_thread::sleep_for(std::chrono::seconds(2));
    return 678;
  };
  std::cout << "[run] after" << std::endl;
  co_return result;
}

int
main(int argc, char** argv)
{
  auto co = run();
  while (!co.handle.done()) {
    co.handle.resume();
  }
  std::cout << "[main]" << co.Value() << std::endl;
  return 0;
}
[promise_type] initial_suspend
[run] before
thread block ? tid#24008
[work]tid#9152
[run] after
[promise_type] return_value
[promise_type] final_suspend
[main]678

実用するには、工夫して promise.set_value が完了してから resume してあげるようにする必要あり。

boost asio の coroutine api を使う

asio の記事に移動しました。

参考

https://qiita.com/tyanmahou/items/522ea1c592db3468940c
https://www.scs.stanford.edu/~dm/blog/c++-coroutines.html
https://downloads.ctfassets.net/oxjq45e8ilak/4kVoTkxYBiVM9lPBZiG2HO/a5e36dc80fd898885269bd6320c96196/Pavel_Novikov_Uchimsya_gotovit_C_korutiny_na_praktike_2020_06_28_18_49_49.pdf

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?