FindAsync() FirstOrDefaultAsync()の利用方法


それぞれ Find()、FirstOrDefault()の非同期版です。
例えば以下のように、 EntityFramework を使用して、 User テーブルにあるデータを 2 つ取得し、取得した内容からビューに表示する文字列を設定するようなプログラムがあったとすると、

public IActionResult Index(int Id)
{
    using(var db = new DataBase())
    {
        var data1 = db.User.Find(Id);
        var data2 = db.User.FirstOrDefault(data => data.Role == "Admin");
        ViewBag.Message = $"{data1?.Name}さんこんにちは。私は{data2?.Name}です。 ;
        return View();
    }
}

これを下記のように書き換えることができます。

public async Task<IActionResult> Index(int Id)
{
    using(var db = new DataBase())
    {
        var data1 = await db.User.FindAsync(Id);
        var data2 = await db.User.FirstOrDefaultAsync(data => data.Role == "Admin");
        ViewBag.Message = $"{data1?.Name}さんこんにちは。私は{data2?.Name}です。 ;
        return View();
    }
}
  • Find、FirstOrDefault がそれぞれ FindAsync、FirstOrDefaultAsync に変わり、非同期メソッドの完了を待機するawaitが追加されました。
  • awaitを使用するため、public IActionResultpublic async Task<IActionResult>とし、非同期メソッドとします。

Find と FirstOrDefault は非同期で行われるようになったため、別のスレッドで処理が行われて、その処理中にメインのスレッドが他の処理を実行できるようになりました。
(ただし、スレッドを作成するのはコストがかかるので、場合によってはコスト増になっている可能性もあります。)
処理が行われる順序は変わっていないため、特に何も考えず、この変更を行っても、基本的には問題は起こりません。
しかしこれだと、data1 の取得が完了してから、data2 の取得を始めるため、時間的な効率は変更前とあまり変わりません。
せっかく非同期で行うので、data1 の処理と data2 の処理を並列して行ってみましょう。

public async Task<IActionResult> Index(int Id)
{
    using(var db = new DataBase())
    {
        var task1 = db.User.FindAsync(Id);
        var task2 = db.User.FirstOrDefaultAsync(data => data.Role == "Admin");
        await Task.WhenAll(task1,task2);
        ViewBag.Message = $"{task1.Result?.Name}さんこんにちは。私は{task2.Result?.Name}です。 ;
        return View();
    }
}

FindAsync、FirstOrDefaultAsync の前のawaitがなくなり、Taskとして変数を作成します。(Task を作成したタイミングで処理が開始され、終了を待たずに次の Task を作成しますので、task1 と task2 は並列処理されます。)
await Task.WhenAll(task1,task2)で、全てのTaskが完了するまで非同期に待機します。
完了したら、task1.Result で結果を Viewbag に代入し、View を返します。
これで、データを取得する処理が並列で行われるため、効率的に処理できるようになりました。

しかしこれは、場合によってはできないことがあります。例えば、取得した data1 のデータを使用して data2 に取得するデータが変わる場合などは、data1 の取得が終わってから、data2 の取得を開始する必要がありますので、並列処理にはできません。

なお、data1 と data2 の両方が取得が完了するのを待機する必要がない場合に、await Task.WhenAll(task1,task2)を省くと、Result を使用したタイミングで、そのタスクが完了していなければ、完了するまで自動的に待機するのですが、同期的に待機され、メインスレッドが固まってしまいますので、省かない方がいいと思います。

public async Task<IActionResult> Index(int Id)
{
    using(var db = new DataBase())
    {
        var task1 = db.User.FindAsync(Id);
        var task2 = db.User.FirstOrDefaultAsync(data => data.Role == "Admin");
        ViewBag.user1 = task1.Result;//同期的に待機
        ViewBag.user2 = task2.Result;
        return View();
    }
}

Programming Blog