やりたいこと
以前の記事を書いてみて、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;
}
実行結果
結果、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");
}
実行順: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");
}
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;
}
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で待つと思われる)