Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

[C#]複数のTaskをgolangのselectみたいに待ちたい

More than 1 year has passed since last update.

C#で複数のTaskを待つ時はTask.WhenAnyを使用します。

Task.WhenAny
async Task HeavyTask() { await Task.Delay(3000); }

var heavyTask = HeavyTask();
// 最初に完了したTaskが戻り値となる
var completed = Task.WhenAny(heavyTask, Task.Delay(1000));

// heavyTaskが完了したかどうか
// heavyTask.IsCompletedを調べるのでもよい
if (heavyTask == completed)
{
    Console.WriteLine("done");
}
else
{
    Console.WriteLine("timeout");
}

これでもできなくはないのですが3個以上のタスクを待ちたくなった場合戻り値がある場合は少し面倒です。

golangの場合、複数のchannelを待つ場合は以下のように書けます。

select
// 重い処理が終わった後に値を送信するチャンネル
ch := make(chan string)

go func() {
    // 重い処理 ...
    time.Sleep(3 * time.Second)

    // 結果を送信
    ch <- "hogehoge"
}()

select {
// 処理が終わった場合
case text := <-ch:
    log.Printf("result: %v", text)
// タイムアウトした場合
case <-time.After(1 * time.Second):
    log.Printf("timeout")
}

重い処理が完了するのを待ちつつ100ミリ秒ごとに定期処理をする場合は以下のように書けます。

select2
// 前半部分は同じ

for i := 0; ; i++ {
    select {
    case text := <-ch:
        log.Printf("result: %v", text)
        return
    case <-time.After(100 * time.Millisecond):
        log.Printf("now loading - %v", i)
    }
}

C#でも似た使い勝手でC#らしさを残した書き方ができないか考えました。
最終的に以下のようになりました。

like-select
var finish = false;
var heavyTask = Task.Run(async () =>
{
    await Task.Delay(3000);
    return "hogehoge";
});

for (var i = 0; !finish; i++)
{
    finish = await MochiTask.Switch(
        async c =>
        {
            var text = await c.Case(heavyTask);
            Console.WriteLine($"result: {text}");
            return true;
        },
        async c =>
        {
            await c.Case(Task.Delay(100));
            Console.WriteLine($"now loading - {i}");
            return false;
        }
    );
}

switch.gif

すこし奇妙なコードですがgolangとだいたい同じになりました。
awaitを使用しているのがC#らしさでしょうか。

動くコードはここのサンプルにあります。
(テストしていないのでバグってるかもしれませんが…)

解説

この機能を解説する上で重要なポイントは非同期メソッドとAwaiterの連携です。

C# 7.0から非同期メソッドの戻り値として任意の型を返すことができるようになりました。
MochiTask.Switchに渡す非同期メソッドの戻り値の型がSwitchCase<T>になっています。
SwitchCase<T>はメンバ変数として非同期メソッド内で初めてawaitされたISwitchCaseCondition型の変数を持っています。
ISwitchCaseConditionc.Caseの戻り値であり任意の一つのタスクを持ちます。
また、外部から継続を実行/キャンセルを差し込む機能を持っています。

MochiTask.Switchは複数のSwitchCase<T>からISwitchCaseConditionを取り出し、完了しているISwitchCaseConditionには継続を、完了しなかった側にはキャンセルを差し込みます。
これによって複数のタスクから一つだけを選択して継続を実行するということを実現しています。

最後に

実用的かと聞かれると微妙ですがなかなか面白いのではないでしょうか。
サンプルを見てすぐに実装が思い浮かんだ人はとても強いと思います。
(あまり需要がないと思うので)解説は適当に書きましたが興味があれば以下のソースを見てみてください。

yaegaki
アセンブリからWebまで広くやっています。
https://yaegaki.dev/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away