スキップしてメイン コンテンツに移動

[C#]async/awaitの挙動をTask-Likeとawaitableパターンで変えてみる

卒論でそういうことをやったので書いておきます。(卒論で作ったプログラム)

まずはasync/awaitの基礎部分から(全部説明するとは言っていない)
public static async Task Sample()
{
    int simpleResult = await Simple();
    int distributedResult = await Distributed();
}
asyncメソッドはこんな感じ(結構おおざっぱ)ですけども、awaitキーワード(演算子?)の後ろ部分で何らかの処理(スレッドは問わない)を行い、そのawaitキーワードによってasyncメソッドの実行スレッドに返ってくる(正確には同期コンテキストがどのスレッドに戻るか決める)のですが、このawaitキーワードの後ろの部分はawaitableパターンならなんでもいいという話。
また、asyncメソッドの戻り値がTask型になっていますが、ここも(C# 7.0から)Task-Likeであればなんでもいいという話。

というわけで、Task-Likeとawaitableパターンを使えばasync/awaitの挙動を変えれるのですが、公式的にこれを利用しているのがValueTaskです。防御的プログラミングとかで早期リターンした場合などにわざわざTaskを起動させるまでもない場合に即時返却するのがValueTaskです。

Task-Like

まずTask-Likeのほうから。
Task-Likeするにはasyncメソッドの戻り値となるTask'型とそのTask`型がasyncメソッドの展開後どのような動きをするか組み立てるAsyncTaskMethodBuilder`型が必要です。(`は名前が被るので自作する方の型という意味です)

また、現時点でほとんどのプラットフォームではAsyncMethodBuilderAttribute属性も自分で定義する必要があります。
namespace System.Runtime.CompilerServices
{
    public sealed class AsyncMethodBuilderAttribute : Attribute
    {
        public AsyncMethodBuilderAttribute(Type builderType)
        {
            BuilderType = builderType;
        }

        public Type BuilderType { get; }
    }
}
こんな感じ。

Task`型とAsyncTaskMethodBuilder`型はダックタイピングな感じでするのでインターフェースの実装などはしなくてもいいですが、その分実装が間違っていた場合コンパイラーが謎のエラーを叩いてしまうので注意が必要です。

Task`型
[AsyncMethodBuilder(typeof(AsyncTaskMethodBuilder`<>))]
public class Task`<T>
{

    private readonly Task<T> _task;
    private readonly TaskAwaiter<T> _taskAwaiter;

    public Task`(Task<T> task)
    {
        _task = task ?? throw new ArgumentNullException(nameof(task));
        _taskAwaiter = new TaskAwaiter<T>(_task);
    }

    public Task<T> AsTask()
    {
        return _task;
    }

    public bool IsCompleted => _task.IsCompleted;

    public bool IsCompletedSuccessfully => _task.Status == TaskStatus.RanToCompletion;

    public bool IsFaulted => _task.IsFaulted;

    public bool IsCanceled => _task.IsCanceled;

    public TaskAwaiter<T> GetAwaiter() => _taskAwaiter;

    public T Result => _task.Result;
}

AsyncTaskMethodBuilder`型
public class AsyncTaskMethodBuilder`<T>
{
    private AsyncTaskMethodBuilder<T> methodBuilder;
    private Task`<T> task;

    public static AsyncTaskMethodBuilder`<T> Create()
    {
        return new AsyncTaskMethodBuilder`<T>() { methodBuilder = AsyncTaskMethodBuilder<T>.Create() };
    }

    public void Start<TStateMachine>(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
    {
    }

    public void SetStateMachine(IAsyncStateMachine stateMachine)
    {
        methodBuilder.SetStateMachine(stateMachine);
    }

    public void SetResult(T result)
    {
        methodBuilder.SetResult(result);
    }

    public void SetException(Exception exception)
    {
        methodBuilder.SetException(exception);
    }
    
    public Task`<T> Task {
        get {
            return task;
        }
    }

    public void AwaitOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : INotifyCompletion
        where TStateMachine : IAsyncStateMachine
    {
        methodBuilder.AwaitOnCompleted(ref awaiter, ref stateMachine);
    }

    [SecuritySafeCritical]
    public void AwaitUnsafeOnCompleted<TAwaiter, TStateMachine>(ref TAwaiter awaiter, ref TStateMachine stateMachine)
        where TAwaiter : ICriticalNotifyCompletion
        where TStateMachine : IAsyncStateMachine
    {
        methodBuilder.AwaitUnsafeOnCompleted(ref awaiter, ref stateMachine);
    }
}

Task-Likeの最小構成ではないですが、こんな感じにAsyncTaskMethodBuilder型を利用してやれば車輪の再開発にはならないですし、複雑な状態機械処理を考えなくてすみます。

Awaitableパターン

awaitableパターンは比較的簡単でforeachのenumerableのアレと似てます。
public struct TaskAwaiter<TResult> : ICriticalNotifyCompletion
{
    private readonly Task<TResult> _task;

    internal TaskAwaiter(Task<TResult> task)
    {
        _task = task;
    }

    public bool IsCompleted {
        get { return _task.IsCompleted; }
    }

    [SecuritySafeCritical]
    public void OnCompleted(Action continuation)
    {
        continuation();
    }

    [SecurityCritical]
    public void UnsafeOnCompleted(Action continuation)
    {
        continuation();
    }

    public TResult GetResult()
    {
        return _task.Result;
    }
}

こんな感じにして、Task`型にGetAwaiterメソッドを生やしてやっておけばいいです。
また、Task`型によってはTaskAwaiterを自作する必要がないこともあるかと思います。Task型を利用しているとかで。

おわりに

Task-Likeとawaitableパターンでasync/awaitの挙動変えることはまずしないと思います。(自分がしたのはただやりたかったからです)
また、テストするのも大変なのでコアライブラリでしか使われないと思います。(ReactiveExtensionsとか)
もし、実装する場合は僕の卒論プログラムも結構参考になるんじゃないかなと思いますのでちょっとは覗いてやってください()

来週は何の記事書こうかな。。


コメント

このブログの人気の投稿

Stellaris 2.0.1 植民スパムプレイ

さっそくですが、友達との3日がかりのマルチプレイ終わりました。

銀河設定は中サイズ(600)、リング2、中盤の危機75年早く、終盤の危機100年早く、技術・伝統コスト0.5倍、難易度普通って感じでした。

そして、自分のプレイングは機械帝国植民スパム、友達は内向き牧歌でした。
最終的な国力としては機械帝国植民スパムのほうが強く、序盤から中盤は内向き牧歌のほうが強かったです。

機械帝国植民スパム はい、前回も言いましたが2.0から植民スパムゲーです。2.0.2からは前哨地維持費がかかるようになり更なる植民スパムゲーにもなりそうです。
植民スパムするからには、まぁロボットか機械帝国って感じなんですが、今回はゲーム進行が速い設定なので機械製造速度アップにガン振り帝国にしました。生産はエネルギー重視で、直轄地に鉱物惑星・研究惑星があるような感じにしました。
それでも、機械帝国は序盤の成長が遅く、鉱物を大量に消費するので、周りの帝国との不可侵条約はかかせません。直轄地6惑星(母星・エネルギー、エネルギー、研究*3、鉱物)の発展が終われば、今度はセクターの開発です。 まぁ、セクターの開発なんか物資さえ送り込めば勝手にやってくれるのでいいですが、エネルギー重視の生産なので鉱物の面倒は見てやらないとダメです。
帝国所有惑星が20ぐらいになれば、まぁ中銀河(サイズ600)なら敵なしですかね。あとは軍拡するだけです。
中盤の危機と終盤の危機 前述の通り危機はかなり早く発生するようにしたのですが、なんと中盤の危機がいっこうに発生しませんでした。発生したのは終盤の危機に立ち向かってるとき…おい()
終盤の危機は機械帝国プレイだったのでもちろんのようにコンティンジェンシー、中盤の危機は機械の反乱でした。
中盤の危機に関しては機械帝国プレイなので自分はどうでもよかったのですが、終盤の危機はがんばらないとまずかったです。コンティンジェンシーなので機械の生産-40%補正がつくので、スペシャルプロジェクトで解除(自分の研究力500ぐらいでは50か月ぐらいでした)しない限り国力が持ちません。
コンティンジェンシーは艦隊戦力が300k+100k*2の集団が4つほど湧いて100k艦隊は時間とともに増え続けるので急がないといけませんが、解除さえすればあまり余る国力で殴り続けるだけでしたね。終盤の危機の間に…

Stellaris ver2.0 Apocalypse プレイ感想

タイトルの通り大型アプデ後での友人とのマルチプレイでのプレイ感想です。

変更内容に関してはこちらのサイトで日本語訳された内容が公開されています(本当に助かりました)
Simulationian.com - 「Stellaris」開発日記#105――2.0「チェリイ」パッチノート

使用した帝国について 細かい設定は覚えていませんが(確認するのもめんどくさい)、有機生命体で狂信権威・平和主義で国是にアップデート内容の生まれた生命(Life-Seeded)を選択しています。 狂信権威について アップデートにより帝国の領域を増やすには星系ごとに影響力(1回限りに支払い)を使い前哨地を建てていく必要がありました。そこで狂信権威を選択したのですが、プレイスタイルが悪いのか、よく奴隷が不満を抱えストライキ?を起こしてしまいました。これに関しては防衛設備を地上に建て防衛軍を増やすことで対処できますが、タイルを無駄に消費している感じがすごいあります。 影響力が毎月4増えるのは領域を増やすのに好都合でしたが、Life-Seededとの相性が微妙でした(後述)。 Life-Seededについて 自分の種族がガイア型惑星にしか住めなくなるが、母星がガイア型25でスタートします。使用感想としては初期ブーストはいいかな?って感じです。ただ、アップデート後の環境ではプレイスタイルを頑張らなければ弱いかなと。その理由としては、まずアップデート後は研究コスト補正にPOP数が関係なくなり領域星系数が関係あるようになりました。つまり領域が大きければ大きいほどコストが高くなるということ。星系の資源はプレイヤーではどうしようもならない固定資源のようなものですから、それに頼って領域を大きくし過ぎると研究コストがすごいことになってしまいます。植民地を確保して賄おうにもガイア型にしか住めないのでかなり増やしにくいです。 ガイア型にしか住めないというのは、有機生命体であってロボットなら他のタイプの惑星に植民できるというのであれば、Life-Seededはロボットプレイをするしかないのかもしれないですが、これに関しては未検証です。
プレイスタイルについて 今回自分がとったプレイスタイルは要塞でガン守りキメるガチ芋戦略です。アップデートで要塞関連が変わり結構使えるものになったらしいので。 防衛プラットフォームを上限まで…

if(flag == true)はありなのか?なしなのか?

最近バズってるようなので便乗
ちなみに元ネタはこちらだとおもいます→qiita - Javaではif (flag == true)というコードを書いてはいけない

※真偽値としてのネーミングとしてflagはナシだろ~~wという話はナシでお願いします
※以下flag変数は真偽値とします

結論から言うと純粋な真偽値の場合はif(flag == true)、if(flag == false)はナシです

まず、最初に前提を整理します
if文は真偽値(true or false)で判定しなければならないJavaではbooleanKotlinではBooleanC#ではbool 話の簡素化のために整えておかないといけないこともあります JavaではBooleanのことは考えない(理由は省略)(ボクシング次第では考えていいかも)KotlinではBoolean?のことは考えないnullableな場合はtrue or false or nullなのでC#ではbool?のことは考えないnullableな場合はtrue or false or nullなので
前提を整理し終わったので、if(flag == true)はアリなのかナシなのか考えていきましょう。 おそらくif(flag == true)論争では2つのパターン(==演算子で比較するかどうか)に派閥分けされていますが、ここでは3つのパターンに分けます
1つ目のパターン:==演算子で比較する// true比較の場合 if(flag == true) // false比較の場合 if(flag == false) はい、もっともナシなパターンです(理由は後述)。

ナシと言われる理由は前提を振り返ればわかりますがif文の中では最終的に真偽値であればよいのですがflagはそもそも真偽値です。わざわざtrueと==比較して真偽値にする必要はありません。わざわざ==比較することは理論上はパフォーマンスが落ちます。

このパターンがなくならない理由は可読性がまぁまぁいいことです。
冗長であろうがパフォーマンスが理論上悪かろうが可読性があれば正義です(そういう考えもできる)。

自分も昔はこのパターンな書き方を多用していましたが後述の理由によって今はしません(少なくとも意識してはしてないはず)。

2つ目のパターン:否定演算子!を使う// true比較の…