TaskとawaitのデッドロックをTaskで回避する

こんなコードがあるとする。

// 1秒後に「zzz」と返す非同期メソッド
async Task<string> sleepy()
{
    await Task.Delay(1000);

    string message = "zzz";
    Console.WriteLine(message);

    return message;
}

// 受け取ったメッセージを倍にして返すメソッド(悪い例)
string doubleMessage()
{
    Task<string> sleepyTask = sleepy();

    string message = sleepyTask.Result;

    message += message;
    Console.WriteLine(message);

    return message;
}

// 受け取ったメッセージを更に倍にして表示するメソッド
void oneMoreDoubleMessage()
{
    string message = doubleMessage();

    message += message;
    Console.WriteLine(message);
}

出力:

最初の「zzz」すら表示されない。というか固まってる。原因はdoubleMessageメソッドの

    string message = sleepyTask.Result;

で、Task.ResultもTask.Waitと同様に…

Result プロパティは、タスクが終了するまで呼び出し元のスレッドをブロックします。
How to: Return a Value from a Task

つまり、sleepyメソッドはTask.Delayが完了してからawait以降の処理を元スレッドで行おうとするけど、doubleMessageメソッドでTask.Resultが元スレッドをブロックしていて何もできない→Task.Resultはsleepyが完了しないのでブロックを解かない→デッドロック。

Task.Resultの代わりにawaitを使えばデッドロックは起きないけど、awaitではちょっと嫌なときもある。

awaitを使うとメソッドが非同期メソッドになるので、もとの戻り値がTだとしたらTask<T>に変わってしまう。初めからそのつもりで作っていれば問題ないが、すでにあるものにちょっと非同期処理を加えただけで呼び出し元メソッドの型まで変えなきゃならなくなると辛い。

この場合だと、doubleMessageでawaitを使うと戻り値がTask<string>になるので、それを受け取るoneMoreDoubleMessageでの処理も書き換えないといけない。もちろんTask.Resultを使うとデッドロックが起きるので、こっちもawaitを使わないと……モウヤダ。

で、自分でも思いついた簡単な(しょうもないともいう)回避策がこれ。

// 受け取ったメッセージを倍にして返すメソッド(良い例)
string doubleMessage()
{
    // Taskでくるむだけ。
    Task<string> sleepyTask = Task.Run(() =>
    {
        return sleepy();
    });

    string message = sleepyTask.Result;

    message += message;
    Console.WriteLine(message);

    return message;
}

Taskを使ってsleepyの呼び出し自体を別スレッドから行うと、sleepyが戻るのはその別スレッドになるので、Task.Resultのブロックとかち合わない。

これなら呼び出し元メソッドの型を変えず、無事に出力を得ることができる。
出力:
zzz
zzzzzz
zzzzzzzzzzzz

おやすみなさい…。

コメント

  1. ナナシ より:

    configureawait使えばいんじゃね