前回の記事では、
非同期処理を書いたのに、なぜ画面が固まるのか?
という話を書きました。
そこで、
private async void Button_Click(object sender, RoutedEventArgs e)
{
HeavyProcess();
}
のような重い処理がUIスレッド上で動けば、asyncを付けていても画面は固まる、という話をしました。
では、こんなコードにすればいいのでしょうか。
private async void Button_Click(object sender, RoutedEventArgs e)
{
await Task.Run(() =>
{
HeavyProcess();
});
}
これなら重い処理は別のスレッドで実行されるため、UIスレッドはブロックされません。
画面も固まらない。
じゃあ、
重い処理は全部Task.Runで囲めばいいんだ!
……本当に?(笑)
今回は、
「画面が固まったら、とりあえずTask.Run」
と考えてしまう前に知っておきたいことを整理してみます。
そもそもTask.Runは何をしているのか
Task.Runは、指定した処理をスレッドプール上で実行します。
ざっくり書くと、
UIスレッド
↓
Task.Run
↓
別のスレッドで重い処理
という形になります。
たとえば、
await Task.Run(() =>
{
HeavyProcess();
});
とすると、HeavyProcess()をUIスレッドとは別のスレッドで実行できます。
その間、UIスレッドは重い処理を実行していないので、画面の描画やボタン操作などを続けることができます。
つまり、
CPUを長時間使う重い処理をUIスレッドから逃がす
という用途では、Task.Runはとても便利です。
では、何が問題なのでしょうか。
「画面が固まる」の原因を見なくなる
個人的に一番怖いのはここです。
画面が固まった。
そこで、
await Task.Run(() =>
{
問題の処理();
});
とする。
画面が固まらなくなった。
直った!
……ように見えます。
でも、本当に原因は解決したのでしょうか。
たとえば処理の中で、
Thread.Sleep(5000);
していたとします。
これを、
await Task.Run(() =>
{
Thread.Sleep(5000);
});
とすれば、確かにUIは固まりにくくなります。
でも、
5秒間スレッドを止めている
という処理そのものは変わっていません。
UIスレッドではなく、別のスレッドを5秒間止めるようになっただけです。
もし本来やりたかったことが、
「5秒待ってから次の処理をしたい」
だけなら、
await Task.Delay(5000);
で済むかもしれません。
同じ「画面が固まらなくなった」でも、中で起きていることはかなり違います。
##「待つ処理」と「重い処理」は違う
ここを分けて考えると、少し分かりやすくなります。
たとえば、
Thread.Sleep(5000);
は、そのスレッドを5秒間止めます。
一方、
await Task.Delay(5000);
は、5秒間ずっとスレッドを占有して待つ必要はありません。
では、
Calculate();
のようにCPUを使って大量の計算をする処理はどうでしょう。
これは待っているわけではありません。
実際にCPUが仕事をしています。
そのため、
await Task.Run(() =>
{
Calculate();
});
としてUIスレッドから処理を逃がすことには意味があります。
ざっくり整理すると、
処理 考え方
CPUを使う重い計算 Task.Runを検討
ファイル・通信などのI/O待ち 可能なら非同期APIを使う
一定時間待つ Task.Delayなどを使う
とにかく画面が固まった まず原因を見る
つまり、
「時間がかかる処理=Task.Run」ではありません。
何に時間がかかっているのかを見る必要があります。
非同期APIまでTask.Runで囲んでいないか
たとえば、すでに非同期版のAPIが用意されている処理があります。
var text = await File.ReadAllTextAsync(path);
これをわざわざ、
var text = await Task.Run(() =>
{
return File.ReadAllText(path);
});
とする必要は通常ありません。
前者は、
待ち時間を非同期に扱う
ためのAPIです。
後者は、
*同期処理を別スレッドで実行している
だけです。
画面だけを見れば、どちらも固まらないかもしれません。
でも仕組みは同じではありません。
ここを区別せず、
時間がかかる処理は全部Task.Run!
としてしまうと、本来使える非同期APIまで同期処理+Task.Runで書いてしまうことがあります。
Task.Runの中からUIを触ると、今度は別の問題が起きる
もう一つ、WPFではよくある問題があります。
こんなことをするとどうでしょう。
await Task.Run(() =>
{
StatusText.Text = "処理中...";
HeavyProcess();
});
Task.Runの中はUIスレッドとは別のスレッドで動いています。
WPFのUI要素は、基本的にそれを作成したUIスレッドから操作する必要があります。
そのため、
UIを固めないためにTask.Runへ移したら、今度はUIを別スレッドから触って例外になった
ということが起きます。
これを見て、
Dispatcher.Invoke(() =>
{
StatusText.Text = "処理中...";
});
とする。
さらに別の場所でも必要になった。
また√Dispatcher.Invoke√。
気がつけば、
Task.RunとDispatcherだらけ。
……ちょっと嫌な予感がします(笑)。
Dispatcherが悪いわけでも、Task.Runが悪いわけでもありません。
問題なのは、
なぜそのスレッドで処理する必要があるのか分からないまま使い始めること
です。
「画面が固まらない」と「処理が速い」は別の話
ここも勘違いしやすいところです。
HeavyProcess();
を、
await Task.Run(() =>
{
HeavyProcess();
});
にしたからといって、
HeavyProcessそのものが速くなるとは限りません。
たとえば10秒かかる計算なら、Task.Runに移しても10秒程度かかるかもしれません。
変わったのは、
その10秒間、UIスレッドを占有しなくなった
ということです。
つまり、
Task.Runを使った
↓
画面が固まらなくなった
↓
処理が高速化した
ではありません。
正しくは、
Task.Runを使った
↓
重い処理をUIスレッドから外した
↓
UIスレッドが他の仕事をできるようになった
↓
画面が固まりにくくなった
です。
ここは似ているようで、かなり違います。
じゃあTask.Runはいつ使うの?
ここまで書くと、
Task.Runって使わない方がいいの?
と思うかもしれません。
そんなことはありません。
たとえば、
・大量データの計算
・画像処理
・圧縮・展開
・CPU負荷の高い変換処理
など、
CPUを長時間使う同期処理をUIスレッド上で実行したくない
場合には、Task.Runは有効な選択肢になります。
大事なのは、
::「画面が固まったからTask.Run」ではなく、
「この処理はCPUを長時間使うからUIスレッドから外そう」::
と理由を説明できることです。
まとめ
Task.Runを使うことで、UIが固まらなくなるケースはあります。
でも、
「画面が固まったらTask.Runで囲む」
と覚えてしまうのは少し危険です。
まず見るべきなのは、
なぜ画面が固まっているのか。
CPUを使う重い処理なのか。
I/Oを待っているのか。
単純にスレッドを止めているのか。
それとも別の理由なのか。
原因によって、選ぶ方法は変わります。
Task.Runは、
画面を固まらなくする魔法ではありません。
適切な場所で使えば、とても便利な道具です。
だからこそ、
とりあえずTask.Run
ではなく、
なぜここでTask.Runを使うのか?
を一度考えてから使う。
それだけでも、非同期処理のコードはかなり読みやすくなると思います。
そして、
「Task.Runにしたら画面が固まらなくなった」
と
「処理が速くなった」
も、実は別の話です。
このあたりは、また別の記事で書いてみようと思います。
関連記事
非同期処理について、もう少し知りたい方はこちらもどうぞ。
非同期処理を書いたのに画面が固まるのはなぜ?
asyncを付けたのにUIが固まってしまう理由を、UIスレッドとの関係から整理しています。
awaitは待つのに、なぜ画面は止まらないの?
「待っているのに止まらない」というawaitの少し不思議な動きを、できるだけシンプルに整理しています。
処理は順番に動くとは限らない?|同期・非同期の基本
そもそも同期処理と非同期処理は何が違うのか、基本から確認したい方向けです。