C#のawait/async非同期処理およびラムダ式

このあたりに関して、ずっとモヤモヤしているので、いろいろ調べた内容などをメモっておく。

参考文献

C#の同期処理を非同期に書き換える手順などが記載されていてわかりやすい
できる!C#で非同期処理(Taskとasync-await)

ラムダ式の書き方を勉強していると、Funcやdelegateとゴチャゴチャになってきたときにすっきりしたサイト
LINQの前に】ラムダ式?デリゲート?Func<T, TResult>?な人へのまとめ【知ってほしい】

MS公式サイトで公開されている非同期プログラミングについて
非同期プログラミングのベスト プラクティス

C#でTaskを同期的に待つ方法をまとめたサイト
C# Taskの待ちかた集

Taskについて掘り下げた説明が詳しいサイト
Taskを極めろ!async/await完全攻略

Taskについて

  • 非同期処理の戻り値はTaskのジェネリック型で定義する必要がある。非同期処理なので、関数呼び出しするとすぐにこの型で返却される。ただし中身は非同期処理が完了した後で利用できるようになるので、引換券のようなものである。例えばTask<int>であれば、非同期処理が終わったあとにint型になる「int型引換券」ということになる。戻り値がvoid型の場合は、Taskが戻り値の型になる。
private async Task<int> Func01Async() {
    Debug.WriteLine( "start Func01Async" );
    await Task.Delay( 1000 );
    Debug.WriteLine( "end Func01Async" );
    return 99;
}
private void Test(){
    Debug.WriteLine( "start Test" );
    Task<int> ret = Func01Async();
    Debug.WriteLine( "end Test" );
}

非同期でFunc01Asyncがコールされていることがわかる

start Test
start Func01Async
end Test
end Func01Async

awaitを追加すると、戻り値retには値が代入された状態で処理が返ってくる。

ただし、awaitは固まったように待っているわけではなく、いったんTest()がリターンされて、Test()の呼び出し側の処理が進む。そして、Func01Asyncの処理が終了すると、await行から再開するような動きになる。

再開したときにはちゃんとしたint型の戻り値が入っているので、従来通りretを参照できるようになる。

private async void Test(){
    Debug.WriteLine( "start Test" );
    ret = await Func01Async();
    Debug.WriteLine( ret );
    Debug.WriteLine( "end Test" );
}
start Test
start Func01Async
end Func01Async
99
end Test

void型のTaskにはWait()Task<T>型にはResultプロパティがあり、これを利用すると、同期的に処理を待つことができるが、GUIやWebアプリでは推奨されてない(デッドロックが発生するケースがある)

Task.Delay( 1000 ).Wait();
Task<int> ret2 = Func01Async();
int a = ret2.Result;

重たい普通の処理を非同期で実行する

// 重たい同期処理
private void HeavyFunc(int size ){
    for (int i = 0; i < size; i++) {
        Thread.Sleep( 1000 );
        Debug.WriteLine( i );
    }
    return;
}

上記の普通の関数を非同期で実行する場合は、Task.Run()で実行される無名関数内でコールすれば良い。(後述するラムダ式を利用)

Debug.WriteLine( "start Task.Run()" );
Task t = Task.Run( () => {
    HeavyFunc( 3 );
});
Debug.WriteLine( "end Task.Run()" );

実行結果

start Task.Run()
end Task.Run()
0
1
2

ラムダ式について

詳細は上記URLの説明を見るとして、delegateを使って関数への参照を定義していた処理をより簡略化して記述できるようにしたもの。非同期関数の終了処理をインライン化して記述できる。非同期処理とラムダ式の記述は直接関係はないが、いつ終わるかわからない終了処理をインライン化して記述できるのは相性が良い。

非同期関数の型

// 引数なし、戻り値void型
Func<Task> FuncAsync = async () =>
{
    await Task.Delay( 3000 );
};
// 引数なし、戻り値int型
Func<Task<int>> FuncAsync2 = async () =>
{
    await Task.Delay( 3000 );
    return 99;
};
// 引数string型、戻り値void型
Func<string,Task> FuncAsync3 = async (string str) =>
{
    Debug.WriteLine(str);
    await Task.Delay( 3000 );
    return;
};
// 引数string型、戻り値int型
Func<string,Task<int>> FuncAsync4 = async (string str) =>
{
    await Task.Delay( 3000 );
    return str.Length;
};

// 呼び出しかた
int ret = await FuncAsync4("abc");
Debug.WriteLine("ret={0}",ret); // ret = 3

メモ

通信処理をイメージ。実際の処理の代わりにTask.Delay()を使って意図した動きになるか確認する。

以下の場合、connectAsyncを実行すると内部で非同期処理が実行されて、その処理が終了するとcallbackが実行される。

callbackは同期関数なので、内部で非同期処理をawaitできない。非同期処理をそのまま非同期でコールするか、Wait()で待つしかない。もしawaitを使いたいなら、callbackの型をFunc<int, Task<int>>にする。(そうしないといけないケースがあるのか?)

private void Button_Click(object sender, RoutedEventArgs e) {
    Debug.WriteLine( "--- start ---" );
    Task t = connectAsync( "abc", (int stat) => {
        Debug.WriteLine( "start lambda" );
        Debug.WriteLine( "stat={0}", stat );
        Task.Delay(1000).Wait(); // 終了までこのまま処理を待つ
        //Task.Delay(1000); // 非同期で実行されるので、即材に終了する
        Debug.WriteLine( "end lambda" );
        return 2;
    } );
    Debug.WriteLine( "--- end ---" );
}

private async Task connectAsync(string url, Func<int, int> callback) {
    Debug.WriteLine( "start connectAsync" );
    int stat = 1;
    await Task.Delay( 2000 );
    int ret = callback(stat);
    Debug.WriteLine( "ret={0}", ret );
    Debug.WriteLine( "end connectAsync" );
}

結果

--- start ---
start connectAsync
--- end ---
start lambda
stat=1
end lambda
ret=2
end connectAsync

コールバック内部で非同期関数を実行する場合、ラムダ式にasyncを追加して、内部でawaitを使って非同期関数(Task.Delay())をコールする。

connectAsync2のコールバックの型はFunc<int, Task<int>>に変更して、awaitを指定してcallbackを実行する

private void Button_Click(object sender, RoutedEventArgs e) {
    Debug.WriteLine( "--- start ---" );
    Task t = connectAsync2( "abc",  async (int stat) => {
        Debug.WriteLine( "start lambda" );
        await Task.Delay( 2000 );
        Debug.WriteLine( "end lambda" );
        return 99;
    });
    Debug.WriteLine( "--- end ---" );
}

private async Task connectAsync2(string url, Func<int, Task<int>> callback ) {
    Debug.WriteLine( "start connectAsync" );
    int stat = 1;
    await Task.Delay( 2000 );
    int ret = await callback(stat);
    Debug.WriteLine( "ret={0}", ret );
    Debug.WriteLine( "end connectAsync" );
}

結果は前回と同じ動きになる。

--- start ---
start connectAsync
--- end ---
start lambda
stat=1
end lambda
ret=2
end connectAsync

コールバックのネスト

javascriptではよくある記述を真似てみる

private async Task connectAsync(string url, Func<int, int> callback) {
    Debug.WriteLine( "start connectAsync" );
    int stat = 1;
    await Task.Delay( 2000 );
    int ret = callback(stat);
    Debug.WriteLine( "ret={0}", ret );
    Debug.WriteLine( "end connectAsync" );
}

private async Task receiveAsync( Func<string, int> callback) {
    Debug.WriteLine( "start receiveAsync" );
    await Task.Delay( 2000 );
    int ret =  callback( "message" );
    Debug.WriteLine( "ret={0}", ret );
    Debug.WriteLine( "end receiveAsync" );
}

2つの非同期関数をネストして実行する。connectAsyncが成功したら、receiveAsyncの処理を開始するようなイメージ

Debug.WriteLine( "--- start ---" );
Task t = connectAsync( "abc",  (int stat) => {
    Debug.WriteLine( "start connect callback" );
    Debug.WriteLine( "stat={0}", stat );
    if( stat == 1) {
        Task t2 = receiveAsync( (string data) => {
            Debug.WriteLine( "start receive callback" );
            Debug.WriteLine( "data="+data );
            Task.Delay( 2000 ).Wait();
            Debug.WriteLine( "end receive callback" );
            return 3;
        } );
    }
    Debug.WriteLine( "end connect callback" );
    return 2;
});
Debug.WriteLine( "--- end ---" );

結果

--- start ---
start connectAsync
--- end ---
start connect callback
stat=1
start receiveAsync
end connect callback
ret=2
end connectAsync
start receive callback
data=message
start receive callback
ret=3
end receiveAsync
-->