第143回 Task、async、await について

公開日:2018-06-26

1. 概要

Taskを使用して非同期処理を行う方法について説明しています。

2. 動画


3. サンプル1

1. ソース

ソース
public class TaskTest1
{
    public void Test()
    {
        var task = TestTask();
        task.Wait();
    }
    
    private Task TestTask() {
        var task = Task.Run(() => {
            Thread.Sleep(10000);
        });
        
        return task;
    }
}

2. 説明

Task.Run() の引数として渡されたラムダ式が別スレッドで動きます。
task.Wait() で、別スレッドが終了するのを待機しています。

3. サンプル2

1. ソース

ソース
public class TaskTest2
{
    public void Test()
    {
        var task = TestTask();
        Console.WriteLine("1");
    }

    private async Task TestTask() {
        var task = Task.Run(() => {
            Thread.Sleep(10000);

            Console.WriteLine("2");
        });

        await task;

        Console.WriteLine("3");
    }
}

2. 説明

await task; で、別スレッドで動いているタスクの処理が終わっていない場合は、一旦呼び元へ return します。
この時、await で指定したタスクが、メソッドの戻り値として返ります。
その後、別スレッドの処理が終わると、メインスレッドが await の次の処理から復帰して、それ以降の処理を行います。

3. サンプル3

1. ソース

ソース
public class TaskTest2_2
{
    public void Test()
    {
        TestTask();
    }

    private async void TestTask() {

        var task = RunTask();

        string result = await task;
        //string result = task.Result; //結果が出るまで待機。
                                       //呼び元には戻らない。
    }

    private Task<string> RunTask() {
        var task = Task.Run(() => {
            Thread.Sleep(15000);
            return "Hello";
        });

        return task;
    }
}

2. 説明

RunTask() の戻り値が Task<string> となっています。
これは、このメソッド本体の戻り値が Task<string> で、
別スレッドの戻り値が string であることを示しています。
また、Task<string> のタスクを await すると、Task<string> が保持している string が返ります。

await は、
別スレッドのタスクが終了していない場合は、現在実行中のメソッドの呼び元に処理を戻し、
別スレッドのタスクが終了したら、Task の Result の値を返します。
例えば、
string result = await task; の箇所は、別スレッドのタスクが終了すると、
string result = "Hello" の形になります。

3. サンプル4

1. ソース

ソース
public class TaskTest2_3
{
    public void Test()
    {
        var task = TestTask();
        string s = task.Result; //デッドロック
    }

    private async Task<string> TestTask() {
        var task = RunTask();

        string result = await task; 
        //string result = await task.ConfigureAwait(false);

        //_lblResult.Text = "OK"; //エラー
        _form.Invoke((MethodInvoker)(() => {
            _lblResult.Text = "OK";
        }));

        return result; //ここで task.Result に値が入る
    }

    private Task<string> RunTask() {
        var task = Task.Run(() => {
            Thread.Sleep(10000);

            return "Hello";
        });

        return task;
    }
}

2. 説明

上記はデッドロックを起こすサンプルです。
メインスレッド(UIスレッド)で task.Result を取得していますが、ここで Result(別スレッドの戻り値) に値が入るまで待機します。 待機と言う処理をしているため、メインスレッドで他の処理を行うことはできません。 await と異なり、呼び元にも戻りません。

タスクの Result は、await task から復帰した後の return で設定されます。
ここはメインスレッドで動きますが、メインスレッドは最初に Result で待機中となっているため、await から復帰することができないため、Result が設定できなくなります。
これにより、自分自身でデッドロックの状態になります。

デッドロックは、await task.ConfigureAwait(false); とすることで回避できます。
これは、await の復帰後の処理を、別スレッドに行わせます。
但し、UIスレッドではなくなるため、フォームのコントロールは通常の方法では参照できなくなるため、注意が必要です。