6
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

【C#】【Task】Taskでマルチスレッドするときの、async/awaitのある時、ない時の処理の実行順の実験

やりたいこと

以前の記事を書いてみて、Taskの使い方を何となくわかった気になっていたが、人が書いたTaskを使うコードを見て、何をしてるかよくわからなかったので復習したい。とくに、async/awaitをつける、つけないで動きがどう変わるのか、試してみたい。

0.マルチスレッドというが、どこの処理がマルチスレッド(別スレッド)で行われているのか?

コード

各箇所でスレッドIDを表示させて、どこが同じIDでどこが異なるIDなのか見た。

// ボタン押したときの処理
private void Button_Click_5(object sender, RoutedEventArgs e)
{
    Console.WriteLine("スレッドID(ボタンハンドラ):" + Thread.CurrentThread.ManagedThreadId);
    var task1 = TaskFunc();
}

private Task TaskFunc()
{
    var t = Task.Run(() =>
    {
        Console.WriteLine("スレッドID(Task.Run内):" + Thread.CurrentThread.ManagedThreadId);
    });

    Console.WriteLine("スレッドID(TaskFunc内、Task.Run外):" + Thread.CurrentThread.ManagedThreadId);

    return t;
}

実行結果

image.png
結果、Task.Run()の中が、別スレッドになっている。
Taskを返すメソッド(ここではTaskFunc())の中は、画面の処理と同じスレッド。(当たり前?)

async、awaitのあるなしで、実行順番がどのように変わるのか?

以下、1番からいくつかの実験をしているが、上から順番に少しずつコードを変えて(asyncやawaitをつけたり消したりして)実験することにする。

※呼ぶ側:下でいうところのButton_Click_5のこと。画面のボタンのハンドラ。
 呼ばれる側:下でいうところのTaskFuncの中の処理のこと。

1.呼ぶ側、呼ばれる側ともasync/awaitなし

private void Button_Click_5(object sender, RoutedEventArgs e)
{
    Debug.WriteLine($"A");
    var task1 = TaskFunc(1000); // Taskのメソッドを呼ぶ
    Debug.WriteLine($"B");
    Task.WhenAll(task1);        // WhenAll
    Debug.WriteLine($"C");
}

private Task TaskFunc(int millisec)
{
    Debug.WriteLine("D");
    var t = Task.Run(() => {    // Task.Run
        Debug.WriteLine("E");
        Thread.Sleep(millisec); // Task.Run内Sleep
        Debug.WriteLine("F");
    });
    Debug.WriteLine("G");
    return t;
}

実行順:ADGBCEF
ADEGBCF
※何度もやると、上のように何パターンか実行順ができる。
 最初がAD、最後がFだけ1000msSleppするので決まっていて、あとは順番が保証されないと思われる。

2.呼ばれる側にasyncだけつける

private void Button_Click_5(object sender, RoutedEventArgs e)
{
    Debug.WriteLine($"A");
    var task1 = TaskFunc(1000); // Taskのメソッドを呼ぶ
    Debug.WriteLine($"B");
    Task.WhenAll(task1);        // WhenAll
    Debug.WriteLine($"C");
}

private async Task TaskFunc(int millisec)// ★★変化点
{
    Debug.WriteLine("D");
    var t = Task.Run(() => {    // Task.Run
        Debug.WriteLine("E");
        Thread.Sleep(millisec); // Task.Run内Sleep
        Debug.WriteLine("F");
    });
    Debug.WriteLine("G");
}

コンパイルは通るが、ワーニングあり。
image.png

実行順:ADGBECF
    ADEGBCF
    ADGBCEF 
※何度もやると、上のように何パターンか実行順ができる。
 最初がAD、最後がFだけ1000msSleppするので決まっていて、あとは順番が保証されないと思われる。

3.呼ばれる側にasync/awaitつける

private void Button_Click_5(object sender, RoutedEventArgs e)
{
    Debug.WriteLine($"A");
    var task1 = TaskFunc(1000); // Taskのメソッドを呼ぶ
    Debug.WriteLine($"B");
    Task.WhenAll(task1);        // WhenAll
    Debug.WriteLine($"C");
}

private async Task TaskFunc(int millisec)
{
    Debug.WriteLine("D");
    var t = await Task.Run(() => {    // Task.Run // ★★変化点
        Debug.WriteLine("E");
        Thread.Sleep(millisec); // Task.Run内Sleep
        Debug.WriteLine("F");
    });
    Debug.WriteLine("G");
}

実行順:コンパイルエラー
image.png

4.呼ばれる側にasync/awaitつける(戻り値の型を指定してコンパイル通るようにする)

private void Button_Click_5(object sender, RoutedEventArgs e)
{
    Debug.WriteLine($"A");
    var task1 = TaskFunc(1000); // Taskのメソッドを呼ぶ
    Debug.WriteLine($"B");
    Task.WhenAll(task1);        // WhenAll
    Debug.WriteLine($"C");
}

private async Task<int> TaskFunc(int millisec)// ★★変化点
{
    Debug.WriteLine("D");
    var t = await Task.Run(() => {  // Task.Run
        Debug.WriteLine("E");
        Thread.Sleep(millisec);     // Task.Run内Sleep
        Debug.WriteLine("F");
        return 1;                           // ★★変化点
    });
    Debug.WriteLine("G");
    return t;                               // ★★変化点
}

実行順:ADBECFG
    ADBCEFG
    ADEBCFG
※何度もやると、上のように何パターンか実行順ができる。
 最初がAD、最後がFGだけ決まっていて、あとは順番が保証されないと思われる。
 最後がFGになるのは、Task.Runの完了に1秒かかり、その完了をawaitしているから。

5.WhenAllにawaitつけるのを忘れていたのでつける(awaitしないとWhenAllする意味がない)

private async void Button_Click_5(object sender, RoutedEventArgs e)// ★★変化点
{
    Debug.WriteLine($"A");
    var task1 = TaskFunc(1000); // Taskのメソッドを呼ぶ
    Debug.WriteLine($"B");
    await Task.WhenAll(task1);        // WhenAll                  // ★★変化点
    Debug.WriteLine($"C");
}

private async Task<int> TaskFunc(int millisec)
{
    Debug.WriteLine("D");
    var t = await Task.Run(() => {  // Task.Run
        Debug.WriteLine("E");
        Thread.Sleep(millisec);     // Task.Run内Sleep
        Debug.WriteLine("F");
        return 1;                           
    });
    Debug.WriteLine("G");
    return t;
}

実行順:ADBEFGC
    ADEBFGC
※何度もやると、上のように2パターン実行順ができる。

6.呼ぶ側でawaitをつける

private async void Button_Click_5(object sender, RoutedEventArgs e)
{
    Debug.WriteLine($"A");
    var task1 = await TaskFunc(1000); // Taskのメソッドを呼ぶ // ★★変化点
    Debug.WriteLine($"B");
    await Task.WhenAll(task1);        // WhenAll
    Debug.WriteLine($"C");
}

private async Task<int> TaskFunc(int millisec)
{
    Debug.WriteLine("D");
    var t = await Task.Run(() => {  // Task.Run
        Debug.WriteLine("E");
        Thread.Sleep(millisec);     // Task.Run内Sleep
        Debug.WriteLine("F");
        return 1;           
    });
    Debug.WriteLine("G");
    return t;
}

実行順:コンパイルエラー
image.png

7.メソッドをawaitするので、WhenAllする意味がないので削除する

private async void Button_Click_5(object sender, RoutedEventArgs e)
{
    Debug.WriteLine($"A");
    var task1 = await TaskFunc(1000); // Taskのメソッドを呼ぶ 
    Debug.WriteLine($"B");
    //await Task.WhenAll(task1);        // WhenAll  // ★★変化点
    Debug.WriteLine($"C");
}

private async Task<int> TaskFunc(int millisec)
{
    Debug.WriteLine("D");
    var t = await Task.Run(() => {  // Task.Run
        Debug.WriteLine("E");
        Thread.Sleep(millisec);     // Task.Run内Sleep
        Debug.WriteLine("F");
        return 1;           
    });
    Debug.WriteLine("G");
    return t;
}

実行順:ADEFGBC
    この1パターン。

このパターンは、全部の処理を上から順に実施するので、Taskに分けている意味がないと思われる。

8.ここから、呼ばれる側のasync/awaitを削除

private async void Button_Click_5(object sender, RoutedEventArgs e)
{
    Debug.WriteLine($"A");
    var task1 = await TaskFunc(1000); // Taskのメソッドを呼ぶ 
    Debug.WriteLine($"B");
    //await Task.WhenAll(task1);        // WhenAll
    Debug.WriteLine($"C");
}

private Task<int> TaskFunc(int millisec)    // ★★変化点
{
    Debug.WriteLine("D");
    var t = Task.Run(() => {  // Task.Run   // ★★変化点
        Debug.WriteLine("E");
        Thread.Sleep(millisec);     // Task.Run内Sleep
        Debug.WriteLine("F");
        return 1;           
    });
    Debug.WriteLine("G");
    return t;
}

実行順:ADGEFBC
    ADEGFBC
    実際には起きなかったが、ADEFGBCもあり得るとおもう。

やってみてわかったこと

  • マルチスレッドだから当たり前ではあるが、パラで走っている処理は、実行の順番は保証できない(前後する)。
  • Task.Run(...)の実行で、別スレッドが立ち上がってTask.Run()の中に書いた処理が走り出す。

再確認した当たり前のこと

  • 「Task」にawaitをつけることができる。(当然どこにでも書けるわけではない)
  • awaitをつけたTaskは、実行が完了するまで待たれる。(それより下の処理は実行されない)。ここでいうところのTaskは、
    • Task.Run(() => ..)もTask。
    • TaskFunc()が返すのもTask。
    • Task.WhenAll(...)が返すのもTask。
  • awaitをつけていないTaskは、完了を待たれない。(上の例8.では、TaskFunc内のTask.Runの次の"G"の処理は、Task.Runとパラで実施される。)
  • メインスレッド(UIのスレッド)でTaskをawaitしても、UIは固まらない。
  • おそらく、5.の例が、Taskの一般的な使い方だと思う。(ただし、今回はtask1のみだったが、普通は複数のTaskをRunして、それらをWhenAllで待つと思われる)
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
6
Help us understand the problem. What are the problem?