2016年9月13日火曜日

Task async/awaitの挙動チェック AsyncInfo編

Task async/awaitの挙動チェック AsyncInfo編

前回はTaskについて調べた。
今回はUWPアプリで出てくるAsyncInfoについて挙動をチェックする事にした。

前回のTaskをラップしてキャンセルと通知機能を付加する機能なのだが、
スレッドIDはどう割り当てられるのか分からない状態である。
焦点は内包するTask自身がTask.Runするしないの動作、AsTaskした時の動作のチェックである。
AsyncInfoの外側は前回の結果からすると同期処理になっている事からTask.Runしないが正解。
だけどAsyncInfo自身で非同期しているのではないかという疑問があるので調査する事にした。


結果:
 3
 test1=3
 ---
 test2=7
 ---
 test4=7
 ---

・AyncInfo内部で非同期するような処理になっていない(test1)
・AsyncInfoに内包するActionをTask.RunにするとスレッドIDが変わる(test2)
・AsTask().Wait()するとフリーズする(test3)
・AsTask()でスレッドIDが変わらない(test4)

まとめると
・Taskと同じ性質である。
・AsyncInfo内に内包するActionはTask.Runしないこと。



ーーー以下ソースコードーーー


private async void rb2_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine(Environment.CurrentManagedThreadId);
    await AsyncInfo.Run(async (token) =>
    {
        await Task.Delay(1);
        Debug.WriteLine($"test1={Environment.CurrentManagedThreadId}");
    });
    Debug.WriteLine("---");
    await AsyncInfo.Run((token) => Task.Run(()=>
    {
        Debug.WriteLine($"test2={Environment.CurrentManagedThreadId}");
    }));
    //freeze
    //Debug.WriteLine("---");
    //AsyncInfo.Run(async (token) =>
    //{
    //    await Task.Delay(1);
    //    Debug.WriteLine($"test3={Environment.CurrentManagedThreadId}");
    //}).AsTask().Wait();
    Debug.WriteLine("---");
    await AsyncInfo.Run(async (token) =>
    {
        await Task.Delay(1);
        Debug.WriteLine($"test3={Environment.CurrentManagedThreadId}");
    }).AsTask();
    Debug.WriteLine("---");
    AsyncInfo.Run((token) => Task.Run(() =>
    {
        Debug.WriteLine($"test4={Environment.CurrentManagedThreadId}");
    })).AsTask().Wait();
    Debug.WriteLine("---");
}



[C#]async/awaitの挙動について

async/awaitの挙動について


以下のサンプルコードにて動作確認を行ってみた。

実行結果は、

---start---
main1=9
test1=9
test2=12
test3=11
test4=12
test5=12
test6=12
test8=11
test7=11
---end---
test9=11
---check1 start---
main2=9
test1=9
test2=11
test3=12
test4=12
test5=11
test6=12
test8=12
---check1 end---
test9=12
test7=11



〇テスト1:Taskの生成した前後でasync/awaitした場合の挙動を調べる
・Taskを生成されるまで同期処理(test1)
・Taskを生成するとスレッドIDが変わる(test2)
・Taskを生成した内側でasync/awaitするとスレッドIDが変わる(test3, test5)
・Taskを生成した外側でasync/awaitすると同期処理(test4)
・純粋にTaskを生成してStartした場合も同じ結果になる(test6)
・純粋にTaskを生成した場合、async/awaitした数分だけ実行サイクルが遅延する(test7, test8, test9)
・純粋にTaskを生成した内側でasync/awaitすると処理順序を守らない(test7, test9)
→完全な非同期処理を行うならば、Taskの付いた命令は必ず最上位でTask.Runする必要がある。

〇テスト2:awaitではなくwait()した場合の挙動を調べ宇r
・Taskを生成する前でasync/awaitするとフリーズする(test1, test5, test6, test8)
・純粋にTaskを生成した場合、中のactionはasync/awaitする必要がある(test6, test8)
→純粋にTaskを生成した場合、Task内Actionをasync/awaitする必要がある。


まとめると
・必ず最上位だけにTask.Runを使う(その関数名の後ろにTaskやAsyncなど名前を付けて置くと分かりやすい)
・async/awaitはTask.Runされた中でしか使ってはいけない(外で使うとフリーズする)


必ずしも上記結論が100%正しいというわけではないですが、
Taskを使う運用ルールを決めてあげないとあっちでは動いてこっちでは動かないという
モグラ叩き現象が発生する恐れがあるので、一番動きやすく覚えやすい運用ルールを決めた方が良いです。




---以下テストコード---

private async void Form1_Load(object sender, EventArgs e)
{
    Console.WriteLine("---start---");
    Console.WriteLine($"main1={Environment.CurrentManagedThreadId}");
    await test1();
    await test2();
    await test3();
    await test4();
    await test5();
    await test6();
    await test7();
    await test8();
    await test9();
    Console.WriteLine("---end---");
    await Task.Delay(100);
    Console.WriteLine("---check1 start---");
    await check1();

    Console.WriteLine("---check2 start---");
    //test1().Wait();   //freeze
    test2().Wait();
    test3().Wait();
    test4().Wait();
    //test5().Wait();   //freeze
    //test6().Wait();   //freeze
    test7().Wait();
    //test8().Wait();   //freeze
    test9().Wait();
}

async Task check1()
{
    Console.WriteLine($"main2={Environment.CurrentManagedThreadId}");
    await test1();
    await test2();
    await test3();
    await test4();
    await test5();
    await test6();
    await test7();
    await test8();
    await test9();
    Console.WriteLine("---check1 end---");
}


async Task test1()
{
    await Task.Delay(1);
    Console.WriteLine($"test1={Environment.CurrentManagedThreadId}");
}

Task test2() => Task.Run(() =>
{
    Console.WriteLine($"test2={Environment.CurrentManagedThreadId}");
});

Task test3() => Task.Run(async () =>
{
    await Task.Delay(1);
    Console.WriteLine($"test3={Environment.CurrentManagedThreadId}");
});

async Task test4() => await Task.Run(() =>
{
    Console.WriteLine($"test4={Environment.CurrentManagedThreadId}");
});

async Task test5() => await Task.Run(async () =>
{
    await Task.Delay(1);
    Console.WriteLine($"test5={Environment.CurrentManagedThreadId}");
});

Task test6()
{
    var result = new Task(() =>
    {
        Console.WriteLine($"test6={Environment.CurrentManagedThreadId}");
    });
    result.Start();
    return result;
}

Task test7()
{
    var result = new Task(async () =>
    {
        await Task.Delay(1);
        Console.WriteLine($"test7={Environment.CurrentManagedThreadId}");
    });
    result.Start();
    return result;
}

async Task test8()
{
    var result = new Task(() =>
    {
        Console.WriteLine($"test8={Environment.CurrentManagedThreadId}");
    });
    result.Start();
    await result;
}

async Task test9()
{
    var result = new Task(async () =>
    {
        await Task.Delay(1);
        Console.WriteLine($"test9={Environment.CurrentManagedThreadId}");
    });
    result.Start();
    await result;
}

2016年9月7日水曜日

[UWP][C#]非同期処理の処理時間

データを書き込むループを100万回回したときの処理時間。


同期処理 = 1ms
TaskのAsync/Await = 13505ms
TaskのWait() = 7934ms
IAsyncActionのAsync/Await = 14672ms
IAsyncActionのAsTask().Wait() = 26608ms


圧倒的に同期処理は早いです。
API回りで15ms以上掛かる処理にはAsyncな処理になってしまっているのですが、
通信処理って非常に繰り返し実行される部分なので当然処理コストを抑えたいのにTaskを使わないといけないんですよね。。

まぁループのオーダーが100万回なので、
1回当たりの処理コストの単位を[ms]から[ns]に変えてみると良く分かります。

O(1)な同期処理は1ns。当然早いですね。
タスクになると一回のasync/await処理コストの最大で見ると13us~30us
実際にコーディングすると2,3個awaitが発生するので
多くみて100usくらいコストを支払っている事になります。
遅い。。

Androider