並列/並行基礎勉強会でasync/awaitをDisってきた

3/23 に開催された、並列/並行基礎勉強会で「async/await 不要論」という発表をしてきました。

一番言いたかったこと

一番言いたかったことは、実は並列とかとは全く関係ないことです。 それは、言語への機能追加に関することです。

C# は 5.0 で非同期処理のための専用の構文として、async/await を導入しました。 これは、F# が計算一般という抽象度の高いものための汎用的な構文として、コンピュテーション式を採用しているのとは対照的です。 専用の構文を用意するかしないかは、その言語を取り巻く環境次第です。

専用の構文の利点と欠点

専用の構文の利点は、その使用用途が明確であるというところにあります。 そのため、書き方さえ覚えてしまえば、その裏で何がどうなるかなどを一切気にせずに使えてしまえたりします。

欠点は、専用の構文なのでそれ以外には使いにくくなるところです。 例えば、クエリ式はその名の通り、クエリのための構文として導入されており、SQL に近い書き方をしますが、 これをクエリ以外のために使う場合は、どうしても不自然になってしまいます。

これには完全に同意です。 クエリ式が専用の構文としてではなく、汎用的な構文として導入されていれば、async/await は不要だったでしょう。

クエリ式をクエリ式として導入しなければ成功はしなかった、という考え方もあるでしょう。 しかし、クエリ式は単純な構文であれば SQL に似ている、と言えなくもないですが、 複雑なクエリを「SQL に似ている」とするのは辛いものがあり、 クエリ式はクエリの専用構文としての導入でなくとも成功したのでは、という思いがあります。

また、専用の構文をどんどん追加していくと、その言語にはどんどんゴミの山が積みあがっていきます。 言語自体の複雑さもどんどん向上していくのも問題です。 これが限界まで進むと、「便利な機能を追加したいのに既存文法とバッティングして追加できない」ようになります。 そして、C# はかなり限界に近づいてきている言語ではないか、と思うのです。

確かに async/await は便利な機能です。 もう導入されてしまったのだから、使える環境であればどんどん使っていけばいいです。 ですが、これからもこういう機能追加を「是」とし続けると、いずれどこかで限界が来ます。 もうちょっと、ストッパー的な立場の人が必要なのではないかな、というのが個人的な思いです。

async/awaitが死ぬとき

async は Task と強く結びついています。 async を付けたメソッド (やラムダ式) は、Task を返すことしかできません(もしくは何も返さない)。 そのため、async/await は Task が古い方法となったその時に死にます。 Task や現状の async/await で対応できなくなった場合、また新たに構文を導入するのでしょうか?

async/await と互換性を保ったまま、より汎用的な構文を入れる、ということも考えられなくはないです。

以下妄想です。

現状の async/await が Task と結びついているのは、Awaitable パターンが要求するシグネチャに戻り値の型に対する規定がないことから来ています。 つまり、Awaitable パターンを実装する Awaitable/Awaiter に「Task」が出てこないのに、裏で勝手に「Task」にラップされてしまうのです。

// Awaitableパターンが要求するAwaiterの例
public class SomeAwaiter<T> : INotifyCompletion
{
    public bool IsCompleted { get; set; }
    public void OnCompleted(Action cont) { ... }
    // Task<T>ではなく、単なるTを返す
    public T GetResult() { ... }
}

この問題を解決するには、どこかに async キーワードと Task を関連付ける仕組みを用意する必要があります。 例えば、

[ContinuationKeyword(
    "async", "await",
    Builders = new[] {
        typeof(AsyncVoidMethodBuilder),
        typeof(AsyncTaskMethodBuilder),
        typeof(AsyncTaskMethodBuilder<>) })]
public class AsyncKeyword { }

のようなイメージです(AsyncなんとかMethodBuilderGetResultの結果と Task を関連付けています)。 こんな感じで、async/await の位置にくるキーワードをユーザ定義できるようにするわけです。

それなんてコンピュテーション式?という感じですね。 まぁ、実現されることはないでしょうが・・・

それReactive Extensionsでできるよ

こんな感じで、一番言いたかったことは実は構文のお話なのでした(そういう意味では、「async/await 不要論」というタイトルは釣りです)。 なので、「それ Rx でできるよ!」というのは、今回はほとんど言ってません。

一応、40 枚目のスライドで名前を出してはいるんですが、正直 Rx は全然調べてなかったので、名前しか出してません。 その後ちょっと調べてみたら、APM がベースで、TAP も IObservable に変換して使えるみたいです。 なので、F# や async/await は使えないけど、Rx が使える場合は、独自で実装せずに Rx を使う、というのが現実的でしょうか。

マルチコア時代とasync/await

これから「どんどん」コア数が増えていくのか、というと、「それは違うのではないか」とスライドに書きました。 しかし、緩やかにはコア数は増えていくでしょうし、現状、複数コアが使える環境というのは多くあるでしょう。 これにより、「マルチコア時代だから async/await だ!」という意見を見かけることも多くなっていくような気がしています。

これについてはスライド中でも「マルチコア化の流れとは無関係に async/await は重要だ」という話をしました。 たとえ 1コアしかなくても、async/await に意味はあるのです。

今回の発表では、この「マルチコア時代だから async/await だ!」という意見が出てくるのを防ぎたかった、 というのが、二番目に言いたかったことでした。 杞憂であるならそれに越したことはないのですが、一応ここでも明言しておきます。

次回以降

アクターに関する話が最初にちょろっと出ただけだったので、次回があるならそのあたりも聞きたいですね。 それか、CPU 内部の並列の話とか。