23/08/07
最新実装に更新しました。
はじめに
VisualStudio 2019 Communityのほうでもコルーチンが使用できるようになったので早速いろいろ試してみてた。
そんなのねぇよという方は、まずお手元のVisualStudioをアップデートしていただいて、/std:c++latest
をお忘れなく
先に言っておきますと、
この記事はOpenSiv3Dを一緒に扱っていますが、この後例で取り上げるTaskの実装自体はフレームワークにまったく依存してないのでOpenSiv3DにかぎらずC++20環境なら適用できます。
今回のゴールは、コルーチンを理解することというより、
コルーチンを使えばこんなことができる!と知ってもらうことにします。
コルーチンとは?
簡単に言うと処理の途中で中断や再開ができる関数みたいなものですが
ここではコルーチンの詳しい解説をしたいわけではないので参考になるものを張っておきます
https://cpprefjp.github.io/lang/cpp20/coroutines.html
https://www2.slideshare.net/yohhoy/20c20
なんだか難しいと思ったかもしれませんが、安心してください。
利用者が覚えるキーワードは3つだけです。
co_yield
co_await
co_return
そしてこの三つのキーワードを使用した際に、具体的にどのような動作をさせるのかを決めるのはコルーチンライブラリの実装者です。
co_yield co_await
co_yield
、co_await
は処理の中断に使用するするキーワードです
基本的にはどちらも、キーワード単体では使用せず、何かしらの値とセットで使うことになります。
Hoge Coroutine()
{
// co_yieldで中断
{
co_yield 1;
auto a = co_yield 1; // 復帰時に値が返ることもある
}
// co_awaitで中断
{
co_await 1;
auto a = co_await 1; // 復帰時に値が返ることもある
}
}
co_await
にはAwaiterと呼ばれるものを渡すことになる。
このAwaiterが中断するかどうか、中断時、再開時に行う処理や返り値等を決めている。
そのためco_yield
やco_await
を使えば、必ず中断されるというわけではない
上記の例ではint型をわたしているが、これは
- await_transform や
- operator co_await のオーバーロード
を使用することで任意の型をAwaiterの型に変換することができる
(もちろんこれをするのはライブラリ実装者なので、利用者が考えることではない)
co_yield
もco_await
するシンタックスシュガーにすぎず
値をうけとって、Awaiterに変換しているにすぎない
co_await p.yield_value( e )と等価
co_yield
、co_await
はどちらも処理の中断に使用できるが、これらの使い分けは結局のところ実装次第でもあるので、使用するコルーチンごとに変わってきます。
co_return
co_return
は関数のreturn
同様に以降の処理をせず、そこでコルーチンを終了させるキーワードです。
また、co_yield
もco_await
も使用しない際に、これが通常の関数ではなくコルーチンであることを明示する際にも使用します。
// 関数
Hoge Func()
{
return; // error Hoge型の値を返そう
}
// コルーチン
Hoge Coroutine()
{
co_return; // ok (※Hoge型がco_returnでvoidが返せるように実装されているなら)
// 以降の処理しない
}
モチベーション
Unityのコルーチンみたいなもんが欲しい!
IEnumerator Hoge()
{
// Something A
// 1秒待機する
yield return new WaitForSeconds(1.0f);
// Something B
}
Unityを使用したことがある人ならUnityコルーチンを使用したことがある人も多いと思いますが
ゲーム制作などではこのようなdelay処理ができると便利な場面が多々あるので
今回はこのようなことができるのを目指して実装することに
実装しました
Fiber というstructでコルーチンの実装しました。
wandboxでも確認できます。
https://wandbox.org/permlink/NHL66y9GZpGfeKXw
Fiber.hpp
#pragma once
#include <coroutine>
struct IAwaiter
{
public:
virtual ~IAwaiter() = default;
virtual bool resume() = 0;
};
template<class Handle>
concept AwaitableHandler = requires(Handle handle)
{
requires std::same_as<IAwaiter*, decltype(handle.promise().pAwaiter)>;
requires std::convertible_to<Handle, std::coroutine_handle<>>;
};
/// <summary>
/// Yield
/// </summary>
struct Yield
{
constexpr Yield() :
count(1)
{}
constexpr Yield(std::uint32_t _count) :
count(_count)
{}
std::uint32_t count;
};
namespace detail
{
struct YieldAwaiter : IAwaiter
{
YieldAwaiter(const Yield& y):
yield(y)
{}
bool await_ready() const noexcept
{
return yield.count == 0;
}
template<AwaitableHandler Handle>
void await_suspend(Handle handle)
{
handle.promise().pAwaiter = this;
}
void await_resume() {}
bool resume() override
{
return --yield.count > 0;
}
Yield yield;
};
}
/// <summary>
/// operator co_await
/// </summary>
/// <param name="yield"></param>
/// <returns></returns>
inline auto operator co_await(const Yield& yield)
{
return detail::YieldAwaiter{ yield };
}
namespace detail
{
template<class T>
struct PromiseType;
}
/// <summary>
/// Fiber
/// </summary>
template <class T = void>
struct Fiber
{
using promise_type = detail::PromiseType<T>;
using Handle = std::coroutine_handle<promise_type>;
public:
Fiber(Handle h);
Fiber(Fiber const&) = delete;
Fiber(Fiber&& rhs) noexcept;
~Fiber();
public:
/// <summary>
/// 再開
/// </summary>
/// <returns></returns>
bool resume() const;
/// <summary>
/// 完了したか
/// </summary>
/// <returns></returns>
[[nodiscard]] bool isDone() const;
/// <summary>
/// 取得
/// </summary>
/// <returns></returns>
[[nodiscard]] decltype(auto) get() const;
private:
Handle m_coro;
};
/// <summary>
/// operator co_await
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="other"></param>
/// <returns></returns>
template<class T>
auto operator co_await(Fiber<T> other);
/// <summary>
/// 両辺のFiberの完了を待つFiberを生成
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
template<class T, class U>
[[nodiscard]] Fiber<void> operator & (Fiber<T> a, Fiber<U> b);
/// <summary>
/// 両辺のいずれかのFiberの完了を待つFiberを生成
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
template<class T, class U>
[[nodiscard]] Fiber<void> operator | (Fiber<T> a, Fiber<U> b);
/// <summary>
/// 左辺のFiberの完了を待ったあと右辺のFiberを待つ
/// </summary>
/// <typeparam name="T"></typeparam>
/// <typeparam name="U"></typeparam>
/// <param name="a"></param>
/// <param name="b"></param>
/// <returns></returns>
template<class T, class U>
[[nodiscard]] Fiber<U> operator + (Fiber<T> a, Fiber<U> b);
template <class T>
inline Fiber<T>::Fiber(Handle h) :
m_coro(h)
{}
template <class T>
inline Fiber<T>::Fiber(Fiber&& rhs) noexcept
: m_coro(std::move(rhs.m_coro))
{
rhs.m_coro = nullptr;
}
template <class T>
inline Fiber<T>::~Fiber()
{
if (m_coro) {
m_coro.destroy();
}
}
template <class T>
inline bool Fiber<T>::resume() const
{
if (!m_coro) {
return false;
}
if (m_coro.done()) {
return false;
}
// Yield
{
if (auto& pAwaiter = m_coro.promise().pAwaiter) {
if (!pAwaiter->resume()) {
pAwaiter = nullptr;
} else {
return true;
}
}
}
m_coro.resume();
return !m_coro.done();
}
template <class T>
inline bool Fiber<T>::isDone() const
{
if (!m_coro) {
return true;
}
return m_coro.done();
}
template <class T>
inline decltype(auto) Fiber<T>::get() const
{
return m_coro.promise().getValue();
}
template<class T, class U>
inline Fiber<void> operator & (Fiber<T> a, Fiber<U> b)
{
while (true) {
a.resume();
b.resume();
if (a.isDone() && b.isDone()) {
co_return;
}
co_yield{};
}
}
template<class T, class U>
inline Fiber<void> operator | (Fiber<T> a, Fiber<U> b)
{
while (true) {
a.resume();
if (a.isDone()) {
co_return;
}
b.resume();
if (b.isDone()) {
co_return;
}
co_yield{};
}
}
template<class T, class U>
inline Fiber<U> operator + (Fiber<T> a, Fiber<U> b)
{
while (a.resume()) {
co_yield{};
}
while (b.resume()) {
co_yield{};
}
co_return b.get();
}
namespace detail {
template<class T>
struct FiberAwaiter;
}
template<class T>
inline auto operator co_await(Fiber<T> other)
{
return detail::FiberAwaiter{ std::move(other) };
}
namespace detail
{
/// <summary>
/// Promise
/// </summary>
template<class T>
struct PromiseValue
{
void return_value(const T& _value)
{
this->value = _value;
}
const T& getValue() const
{
return value;
}
T value;
};
// void特殊化
template<>
struct PromiseValue<void>
{
void return_void() {}
void getValue() const
{}
};
template<class T>
struct PromiseType : PromiseValue<T>
{
using FiberType = Fiber<T>;
auto get_return_object() { return FiberType{ FiberType::Handle::from_promise(*this) }; }
auto initial_suspend() { return std::suspend_always{}; }
auto final_suspend() noexcept { return std::suspend_always{}; }
void unhandled_exception() { std::terminate(); }
auto yield_value(const Yield& _yield)
{
return operator co_await(_yield);
}
IAwaiter* pAwaiter = nullptr;
};
/// <summary>
/// Awaiter
/// </summary>
template<class T>
struct FiberAwaiter : IAwaiter
{
FiberAwaiter(Fiber<T>&& f):
fiber(std::move(f))
{}
bool await_ready() const
{
return !fiber.resume();
}
template<AwaitableHandler Handle>
void await_suspend(Handle handle)
{
handle.promise().pAwaiter = this;
}
decltype(auto) await_resume() const
{
return fiber.get();
}
bool resume() override
{
return fiber.resume();
}
Fiber<T> fiber;
};
}
見ていただくとわかるように実装者側は多くのカスタマイズポイントを使用してゴネゴネ書きまくらないといけない。
利用者側は以下みたいなコードを書くことになる
Fiber<void> Hoge()
{
// Something A
// 1秒待機する
co_await WaitForSeconds(1.0s);
// Something B
}
今回実装した Fiber の特徴と使い方
メソッドresumeを呼ぶことで処理を再開させる
Fiber<void> Sample()
{
int32 a = 0;
Print << ++a;
co_yield{}; // ただ待機したいときは co_yield{} と記述するだけいいように実装した
Print << ++a;
co_yield{};
Print << ++a;
co_yield{};
}
void Main()
{
auto task = Sample();
task.resume(); // 1を出力
task.resume(); // 2を出力
task.resume(); // 3を出力
while (System::Update()) {
}
}
この特徴を生かし、メインループに混ぜ込むことで
co_yield
を1フレーム待機という意味合いにすることができる
void Main()
{
auto task = Sample();
while (System::Update()) {
// 毎フレーム再開させることで待機は1フレーム待機の意味になる
task.resume();
}
}
ちょうどUnityのコルーチンでもyield return null
は1フレーム待機なのでそれと同じようなイメージにできます。
また、Fiberではresumeを明示的に呼ぶことで処理を進めるその特徴から
処理の中断はresumeを呼ぶのをやめるだけで良いので容易にできます。
(インスタンスの実態は残ってるのでメモリ解放などは起こらないが)
Fiberの中で別のFiberを実行することができる
以下は2秒ごとにカウントアップした数値を表示するコルーチンです
// 指定秒待機
Fiber<void> WaitForSeconds(const s3d::Duration& duration)
{
Timer timer(duration, StartImmediately::Yes);
while (!timer.reachedZero()) {
co_yield{};
}
}
Fiber<void> Sample()
{
int32 a = 0;
while (true) {
Print << ++a;
// 2秒待つ
co_await WaitForSeconds(2s);
}
}
void Main()
{
auto task = Sample();
while (System::Update()) {
task.resume();
}
}
このようにco_await
に別のタスクを指定することで、resumeで再開する際に別のFiber側の処理を進めることができる。
複数のFiberを扱う便利なoperatorオーバーロード
これはほぼ遊びで付けた要素だが、せっかくなので紹介する
operator &
複数のFiberを並行で実行し、両方完了したら完了
(スレッドを分けてるわけではないので、左辺が必ず先に処理させる)
Fiber<void> Sample()
{
Print << U"1";
co_yield{};
Print << U"2";
}
Fiber<void> Sample2()
{
Print << U"A";
co_yield{};
Print << U"B";
co_yield{};
Print << U"C";
}
void Main()
{
auto task = Sample() & Sample2();
while (System::Update()) {
task.resume();
}
}
// 出力
1 A 2 B C
これもまたFiberなので別のFiber内からco_yieldで待つことも可能
operator |
複数のFiberを並行で実行し、いづれか完了したら完了
(スレッドを分けてるわけではないので、左辺が必ず先に処理させる)
Fiber<void> Sample()
{
Print << U"1";
co_yield{};
Print << U"2";
}
Fiber<void> Sample2()
{
Print << U"A";
co_yield{};
Print << U"B";
co_yield{};
Print << U"C";
}
void Main()
{
auto task = Sample() | Sample2();
while (System::Update()) {
task.resume();
}
}
// 出力
1 A 2 B
operator +
右辺のタスクを左辺の後に実行
Fiber<void> Sample()
{
Print << U"1";
co_yield{};
Print << U"2";
}
Fiber<void> Sample2()
{
Print << U"A";
co_yield{};
Print << U"B";
co_yield{};
Print << U"C";
}
void Main()
{
auto task = Sample() + Sample2();
while (System::Update()) {
task.resume();
}
}
// 出力
1 2 A B C
任意の型を返すことができる
co_return
で返した値をget()
メソッド取得可能
Fiber<int> Sample()
{
co_await WaitForSeconds(2s);
co_return 10; // co_returnで10を返す
}
void Main()
{
auto task = Sample();
while (System::Update()) {
ClearPrint();
if (!task.resume()) { // Fiberが完了するとresumeがfalseを返す
Print << task.get(); // 出力 10
}
}
}
Fiber内からであればco_await
してそのまま値を受け取れる
Fiber<int> Sample()
{
co_await WaitForSeconds(2s);
co_return 10;
}
Fiber<void> Sample2()
{
int32 a = 0;
while (true) {
a += co_await Sample();
Print << a;
}
}
void Main()
{
auto task = Sample2();
while (System::Update()) {
task.resume();
}
}
// 出力
10 20 30 ...
サンプルプリセット紹介
Fiberで実装したコルーチンのサンプルとしていくつか紹介しておきます。
WaitForSeconds
指定秒まつ
[[nodiscard]] Fiber<void> WaitForSeconds(const s3d::Duration& duration)
{
Timer timer(duration, StartImmediately::Yes);
while (!timer.reachedZero()) {
co_yield{};
}
}
WaitForFrame
指定フレームまつ
[[nodiscard]] Fiber<void> WaitForFrame(s3d::uint32 frame)
{
co_yield{frame};
}
WaitUntil
条件をみたすまで待つ
template<class Pred, std::enable_if_t<std::is_invocable_r_v<bool, Pred>>* = nullptr>
[[nodiscard]] Fiber<void> WaitUntil(Pred pred)
{
while (!pred()) {
co_yield{};
}
}
WaitWhile
条件をみたす間待つ
template<class Pred, std::enable_if_t<std::is_invocable_r_v<bool, Pred>>* = nullptr>
[[nodiscard]] Fiber<void> WaitUntil(Pred pred)
{
while (pred()) {
co_yield{};
}
}
Async
別スレッドで処理し完了するまで待機
template<class Fty, class... Args>
[[nodiscard]] auto Async(Fty func, Args&&... args)->Fiber<decltype(func(std::forward<Args>(args)...))>
{
auto f = std::async(std::launch::async, std::move(func), std::forward<Args>(args)...);
while (f.wait_for(0s) != std::future_status::ready) {
co_yield{};
}
co_return f.get();
}
Fiberの使える場面
やはりディレイ処理など時間に関与するものを実装する際は便利です
- イージング等のアニメーションの組み合わせ
- キャラクターの攻撃パターンロジックの実装
- 定期実行する処理
- etc...
まとめ
- C++20コルーチンを使えば↑みたいな感じでプログラミングができる
- 3つのキーワード
co_yield
,co_await
,co_return
- 3つのキーワード
- コルーチンを使うことでdelay処理などが簡単にでき、ゲームプログラミング等とは相性がよい場面がある
- 今回紹介したFiberは1例にすぎません、自分だけの最強コルーチンを作ってみてください。
- 実装者はちょっと大変。使用者には優しく。