2016の11日目の記事です。皆さん、こんにちは、こんばんは、おはようございます。natsu1211です。仕事ではゲームクライアントの開発をやっています。今日はcoroutineのあれこれについて語りたいと思います。前書きさて、ゲーム開発をする以上、スプライト/モデルを制御する必要が必ず出てくる。ある引っ張りハンティングRPGSTGゲームのボスを以下のように制御したいと仮定しよう。}}こういう風なコードを書きたいですが、updateのなかでずっと待っていると(blockingな処理)、ほかの処理ができなくなり、ゲームが止まってしまいます。そのため、一旦この関数から脱出して、ほかの処理をしに行かないといけないです。皆さんがご存知の通り、c/c++などの言語での関数の呼び出しでは、stackFrameにレジスタの値、引数、ローカル変数などのコンテキストを保存しています。呼び出された関数からreturnしたあと、その関数のstackFrameがクリアされます。呼び出された関数が実行途中でreturnして、もう一回呼び出して、returnの後の位置から処理を再開するといった操作ができない。必然に、メンバー変数なり、何らかの形で現在ボスの状態を保存しないといけないです。現実:大体こんな感じのコードなってしまいます(あくまで例なので、汚いです><)。}}うーん、switch-case文の中にバラさせられてしまい、処理の流れが見えづらくなり、状態の管理もしないといけなくなった(非同期処理の面影が出てきたんじゃないでしょうか)。いい方法はないでしょうか?ちょっと待って、Unityならこういうコードを書けるよ(あくまで例なので、汚いです><)。}}startCoroutineという起動関数が若干気になりますが、かなり理想の形に近づきましたね(処理の流れを沿ってそのまま書ける)。これはまさにcoroutineの威力です。coroutineとは随分と長い前書きとなりました。ここからは本題に入ります。routine(協調するルーチン)の意味です。1つ重要なポイントはcoroutineはもっと一般化したルーチンです。普通のルーチンの機能を加え、ほかのcoroutineと協調して、制御権を譲り合う(yield)ことができる。関数の呼び出しではできない、実行を一旦中断して、制御を外部に渡し何らかの処理を行い、また前回中断したところから実行を再開することができる。主従関係を持つ関数の呼び出しとは違い(呼び出されるほうの実行は常に先に終わる)、coroutineの間の「呼び出し」は平等な関係にある。細かく言うと、coroutineは以下の特徴を持つ。対称と非対称の制御権受け渡し対称とは呼び出されるほうのルーチンもyieldで制御権を譲る。呼び出されるほうのルーチンもyieldで。呼び出すのルーチンに戻すわけではないので、yieldの対象を明記する必要がある。非対称とは呼び出されるほうのルーチンはyieldではなく、resume構文で呼び出すのルーチンに戻す。2つの表現力は同じことはすで証明されているが、対称coroutineのyieldは濫用されプログラムの処理順番が分かりづらくなる恐れがあります。非対称のほうが見通しがよいため、広く使われている。coroutineは機能面で制限されているが、メモリの消耗が少ない(4KB〜数十KB程度)。go,rubyのcoroutineはこのタイプですそして、coroutineを使った典型的なコントロールフローはマルチスレッドのとかなり近いですが、threadはprocess上に存在するに対し、coroutineはthread上に存在しています。threadはOSによってスケジュールされているため、thread自身がスケジューリングに気にする必要がありません。coroutineはユーザースペースでのただのルーチンのため、自分自身でスケジューリングをしないといけません(制御権を譲ることを明言する)。逆に言うと、マルチスレッドプログラムとは違い、ユーザーがプログラムの実行フローを完全にコントロールすることができる(coroutineの数が多くなると、スケジューラでcoroutineの制御権をスケジューリングする場合も多くある。さらcoroutineを複数のスレッドの間で移動させることも可能です。Goroutineのスケジューラーの実装はいい例です。coroutineの中身を覗いてみるそれでは、いったいcoroutineはどうやって前述の挙動を実現したのか。言うば単純ですが、coroutineが制御権を譲る前でその時のコンテキストを全部保存して、処理を再開したいときにコンテキストを復元できれば、前述の挙動が実現できるわけです。そこで答えなければならない問題が2つあります。レジスタの値、ローカル変数、呼び出し階層などの情報がすべて含まれているため、コンテキストと呼び出し階層を完全に復元することは可能。coroutineに対してはローカル変数だけです、呼び出し階層を保存していないため、coroutineの内部で呼び出されたルーチンでは制御権を譲ることはできないです。coroutineは通常heapでstackを作ります。coroutineはスクープ内のローカル変数をメンバー変数あるいはstatic変数として保存します。coroutineです。コンテキスト保存の仕事を担うのはyield構文です。動きから見ると、yieldをするたびに、generator関数の実行が一旦中止し、外部に実行権を譲ります。外部でresume意味を持ち構文で実行権をgenerator関数に戻し、次のyieldまで実行する。}}ご存知の方も多いと思いますが、Genクラスの内部に、コンパイラが新しい私有Enumeratorを作ってくれます。yieldするものはcurrentとして保存しています。外部からみるとmoveNext()は非対称coroutineのresume構文として見られる。内部的にmoveNext()は状態機械になっていて、foreachでmoveNext()がfalseを返すまで呼び続けます。false;}javascriptのほうはfacebook製のregeneratorを使い、コンパイル後のコードを見てみましょう。Example();console.log(g.next().value);regeneratorRuntimeも絡んでいるので、完全に説明すると長くなるため、詳しい説明は割愛しますが、_contextにコンテキストを保存していると、雰囲気的に分かれるかと思います。コンパイラはいかにしてgenerator関数を状態機械に変換したのでしょうか。基本的はgenerator関数に対してCPS変換を行い、継続を状態に更に変換し、自動的に状態機械を作っています。yield構文がない言語でのcoroutine実装schemeには強力なcall/ccがあるので、coroutineが難なく作れますが、c/c++は少々骨が折れる。偉大な先人たちはすでに多くの仕事をしました。deviceという実装は基本です。マクロとswitch-case文だけで状態機械を作り(闇です...)、generator関数を実現しました。static変数で状態を保存していますので、基本シングルスレッドでしか使えません。deviceを利用しました。ほかの標準化提案もあります。coroutineの実装Csetjmp/longjmpは基本です。setjmp/longjmpはいわばCにおいてのcall/ccの弱化版で、setjmpで現在のコンテキストをjmp_bufという構造体のインスタンスenvに保存しています。当たり前ですが、setjmp/longjmpの実装とjmp_bufの中身はアーキテクチャによって異なります。longjmpでenvで保存されているコンテキストを復元する。第二引数のvalueがsetjmpの戻り値になる。これは割りと本質的なところを触れているので、ちょっと中身を見てみましょう。.-md_cxt_save突き詰めところ、コンテキストの保存はいわばこういうことです。threadだとOSがやってくれますが、coroutineだとユーザー自分でやらないといけないです。coroutineの実装も同じようなことをやっているはずです。次に、longjmpのi386においての実装。.-_st_md_cxt_restoreコンテキストの復元です。これをみると、なぜlongjmpの第二引数valueの値がsetjmpの戻り値になるのも明らかになりましたね。初回呼び出しでのsetjmpの戻り値が必ず0で、二度目以降の戻り値はvalue、つまりeaxの値になります。ucontext、setjmp/longjmpの強化版、現在実際プロジェクトに使われているCのcoroutineライブラリは基本ucontextを使って実装されたものです。cloudwu/coroutineC++Boost.Coroutine2、Boost.Contextに依存。こちらも内部的にアセンブリ言語ほかの標準化提案もあります(stackless派とstackfull派の戦争...)。coroutineを実装するということは、yield意味を持つ構文を自動的に状態機械に変換してくれる機構を実装することです。coroutineはコンテキストスイッチのためレジスタを操作する必要があり、低レベルのアセンブリ言語の手助けが必要。async/awaitへESNextで入るキーワードです。5.0ですでに出来ているですが)。これもcoroutineに深い関係があります。javascriptでは、async/awaitは以下のように変換されます。}spawnはcoみたいな起動関数です(内部的にnext()が再帰的に呼ばわれる。promiseオブジェクトが帰ってくる)。コンパイラが起動関数を用意してくれるため、サードパーティのライブラリを依存する必要もなくなり、簡潔になりました。コードの見通しがさらに良くなりました。asyncは関数の中に非同期処理があること,awaitはこの処理は非同期処理であることを示しています。yieldより意味が明確です。まとめcoroutineは強力なフローコントロール能力を提供してくれました。非同期処理みたいな本来一つのまとまった処理が分割処理しないといけない場合、coroutineをうまく活用すれば、意図通りのコードを書けることができ、見通しと保守性が一段と上がります。クライアントに限らず、concurreny性能が求められるサーバーでもcoroutineが威力が発揮します。coroutineはthreadより軽量的で、コンテキストスイッチのオーバーヘッドが少ない。同じメモリでより多くのcoroutineを作ることができ、concurreny性能の向上が期待できる。upLoginユーザーは見つかりませんでした。