Task『一体いつから… IsCompleted == true でタスクが完了したと錯覚していた?』 わい「なん…だと……」で死亡

結論:単に自分のバグだっただけなんですけども(ってか Task をニワカで使ってるのが悪い)




以下のようなコードで以下のような出力が得られました:

// C#
static void Main(string[] args)
{
    Task task = Task.Run(() => daruiTask());
    Task.Delay(5).Wait();
    Console.WriteLine($"{nameof(task.IsCompleted)} ; {task.IsCompleted}");
    task.Wait();
    Console.WriteLine($"{nameof(task.IsCompleted)} ; {task.IsCompleted}");

    Console.WriteLine($"かえる");
    Task.Delay(1000).Wait();
}

static async void daruiTask()
{
    await Task.Delay(100);
    Console.WriteLine($"おしごと完了ヾ( ゚∀゚)ノ゙");
}

/**
stdout
======
IsCompleted ; True
IsCompleted ; True
かえる
おしごと完了ヾ( ゚∀゚)ノ゙
**/

「かえる」の後に「おしごと完了ヾ( ゚∀゚)ノ゙」しやがったー!!(ガビーン …ってか、IsCompleted が 常に true とは一体ウゴゴゴゴ…

って感じなんですが、結論としては daruiTask() の戻り値が async void なのが悪いです。はい。 散々使うなよ!って言われ続けてるもの使っちゃったのが悪いです。はい。 async Task なら問題なく待ってくれます。はい。 本番コードで実装している際「戻り値不要だわ。void と…」って関数作った後に「async にしたほうがよくね!?」とか夜なべ中に軽くパニクりながら書いてたのが原因でした。はい。 すいませんでしたァ!!


↓問題ないコード

// C#
static void Main(string[] args)
{
    Task task = Task.Run(() => daruiTask());
    Task.Delay(5).Wait();
    Console.WriteLine($"{nameof(task.IsCompleted)} ; {task.IsCompleted}");
    task.Wait();
    Console.WriteLine($"{nameof(task.IsCompleted)} ; {task.IsCompleted}");

    Console.WriteLine($"かえる");
    Task.Delay(1000).Wait();
}

static async Task daruiTask()
{
    await Task.Delay(100);
    Console.WriteLine($"おしごと完了ヾ( ゚∀゚)ノ゙");
}

/**
stdout
======
IsCompleted ; False
おしごと完了ヾ( ゚∀゚)ノ゙
IsCompleted ; True
かえる
**/

async void を食わせると、無条件で IsCompleted が true 返すんですねぇ… そりゃそうか


Task でコールバックする関数を async にする意味とは?

実のところ、これがあんまり見えてないです。(上記の話題と関係あるようであんまり関係ない話なんですけど)

GUIスレッドから async な関数呼ぶのは理解できるんですよ。 「他のスレッドに処理を委譲して、俺は一旦帰ってメッセージポンプ回してくるわぁー。 よろしくなー!」って感じで。

それじゃぁ、Task でコールバックして既に他のスレッドに委譲している時に、非同期関数で更に別のスレッドに委譲しても良い状況にするとはなんぞやと。 別に GUI が固まるわけでも無いしなぁ…と。

個人的には、Taskがコールバックしたasync関数の中で await した → なんか重たい処理が走る → Taskの呼び出し元のタスクスケジューラーさんに一旦戻って、良い感じにスレッド/タスクをマネジメントしてもらってね! …ってのはアリなような気はしていて、そんな意識の元 async な関数を Callback するのもアリなんじゃないかと勝手には思っています……が、.NET Framework 側でホントにそんなステキな事やってくれるのか全くの未確認(ぉ

# 非同期関数投げるんだったら、上記コードでも Task.Run(async () => await daruiTask()) にしないとダメとか、そんなら lambda 噛まさず直接関数投げろよとか色々ツッコミ所はあるんですがまぁ…まぁ……


じゃぁ、Task で Callback する関数内は同期関数だけに絞る? …まぁ普通にアリなのではと思います。 Task は「呼び出し元が継続するのは重たすぎてブロッキングしちゃうから処理を委譲する」わけで、ブロッキングする重たい処理を好きにすればいいじゃん。みたいなー。ていうかー

ただ、微妙に気になるのは CancellationToken で要キャンセルな環境になった時ですが…

Task cookTask = Task.Run(() => CookFile(srcFile));

public void CookFile(FileInfo srcFile)
{
    byte[] dekaiBuff = Cook(srcFile);
    using(FileStream fs = new FileStream(@"\\hayai-svr\c\spool", FileMode.Create, FileAccess.Write))
    {
        fs.Write(dekaiBuff, 0, dekaiBuff.Length);
        // ↑"速い"と思ったら、LAN自体は 10Base-5 で死亡
        // 1GB を転送しようとして 2.5時間ぐらい帰ってこない!
        // ネットワーク管理者出てこいよ!!!!
        // # 一気に 1GB 書くなよって話は勘弁してください(ぉ
    }
}

↓こんな感じにすりゃ、なんか良い感じに kill ってくれる…?(書いてて無理・危険な気がしてきた…

Task cookTask = Task.Run(() => CookFile(srcFile), cancelToken);

↓こっちの方がまだお行儀良さそう…?

Task cookTask = Task.Run(() => CookFile(srcFile, cancelToken)), cancelToken);

public void CookFile(FileInfo srcFile, CancellationToken cancelToken)
{
    byte[] dekaiBuff = Cook(srcFile);
    using(FileStream fs = new FileStream(@"\\hayai-svr\c\spool", FileMode.Create, FileAccess.Write))
    {
        fs.WriteAsync(dekaiBuff, 0, dekaiBuff.Length).Wait(cancelToken);
        // ↑同期関数内だけど、非同期関数を cancelToken 付きでブロッキング
    }
}


Task わかんねーわ