前回の記事では、
同期処理と非同期処理の違い
について紹介しました。
簡単に振り返ると、同期処理は、
処理A
↓
処理Aが終わるまで待つ
↓
処理B
という流れ。
一方、非同期処理では、
処理Aを開始
↓
待っている間、他の処理を進められる
↓
処理Aが完了
↓
続きの処理
という考え方を紹介しました。
では、C#ではどうやってこのような処理を書くのでしょうか。
そこで登場するのが、
async / await
です。
例えば、
var result = await GetDataAsync();
こんなコードを見たことがある人もいると思います。
でも、初めて見ると少し不思議です。
awaitは、
「待つ」
という意味です。
だったら、
待っているなら同期処理と同じじゃないの?
と思いませんか?
今回は、
awaitは待っているのに、なぜ画面は止まらないのか?
というところから、async / awaitの基本を整理してみます。
まず「2つの待ち方」を比べてみる
最初に、今回一番大事なところを見てみます。
例えば、
5秒待ってから処理を続けたい
とします。
こんな書き方があります。
Thread.Sleep(5000);
そして、こんな書き方もあります。
await Task.Delay(5000);
どちらも、
5秒待つ
という点では同じように見えます。
でも、この2つ。
実は待ち方が違います。
Thread.Sleepは「その場で待つ」
例えばWPFで、
private void Button_Click(object sender, RoutedEventArgs e)
{
StatusText.Text = "処理中...";
Thread.Sleep(5000);
StatusText.Text = "完了しました";
}
と書いたとします。
この場合、
ボタンを押す
↓
Thread.Sleep
↓
5秒間UIスレッドが止まる
↓
処理を再開
となります。
WPFなどのUIアプリでは、画面の描画やユーザー操作への応答など、多くのUI処理をUIスレッドが担当しています。
ところが、
Thread.Sleep(5000);
をUIスレッドで実行すると、そのスレッド自体が5秒間ブロックされます。
その間、
・画面を再描画する
・ボタン操作に反応する
・マウス操作に反応する
といった仕事もできません。
そのため、
画面が固まったように見えます。
Task.Delayは「その場を占領して待たない」
では、こちらはどうでしょう。
private async void Button_Click(object sender, RoutedEventArgs e)
{
StatusText.Text = "処理中...";
await Task.Delay(5000);
StatusText.Text = "完了しました";
}
同じように5秒待っています。
でも、
await Task.Delay(5000);
では、その5秒間ずっとUIスレッドをブロックしているわけではありません。
イメージとしては、
ボタンを押す
↓
Task.Delayを開始
↓
await
↓
まだ終わっていないので、いったんこの処理を中断
↓
UIスレッドは他の仕事をできる
↓
5秒経過
↓
awaitの続きから再開
↓
「完了しました」
となります。
つまり、
Thread.SleepもTask.Delayも「5秒待つ」。
でも、
Thread.Sleep
→ スレッドを止めて待つ
await Task.Delay
→ 処理をいったん中断し、スレッドを占有せずに待つ
という違いがあります。
今回の記事で一番覚えてほしいのは、ここです。
「処理の完了を待つ」ことと、
「スレッドを止めて待つ」ことは同じではありません。
では、asyncとは?
ここでasyncを見てみます。
private async void Button_Click(object sender, RoutedEventArgs e)
{
await Task.Delay(5000);
MessageBox.Show("完了しました");
}
asyncは、最初のうちは、
「このメソッドではawaitを使って非同期処理を書ける」
と考えると分かりやすいと思います。
ここで注意したいのが、
asyncを付けただけで、そのメソッド全体が別スレッドで動くわけではない
ということです。
async
と書けば、自動的に別のスレッドが作られて、そこで全部処理してくれる。
というわけではありません。
awaitは何をしている?
では、
await
は何をしているのでしょうか。
例えば、
var result = await GetDataAsync();
なら、
GetDataAsyncを開始
↓
まだ終わっていない
↓
いったんこのメソッドの実行を中断
↓
GetDataAsyncが完了
↓
awaitの続きから再開
↓
resultを使った処理へ
という流れになります。
つまりawaitは、
非同期処理が完了するまで、その後の処理を進めない
という意味では確かに「待っています」。
でも、
その待ち時間にスレッドまで止めているわけではありません。
ここが同期的な「待つ」と大きく違うところです。
コックさんの例で考えてみる
前回の記事では、料理に例えて同期・非同期を考えました。
今回も同じ例で見てみます。
Thread.Sleepのような待ち方
コックさんがお肉をオーブンに入れます。
お肉をオーブンに入れる
↓
焼き上がるまでオーブンの前で待つ
↓
10分経過
↓
お肉を取り出す
↓
次の仕事へ
お肉が焼けるまで、
コックさん自身がその場を離れません。
これが、スレッドをブロックして待っているイメージです。
awaitのような待ち方
今度は、
お肉をオーブンに入れる
↓
「焼き上がったら続きをする」
↓
コックさんは、その仕事からいったん離れる
↓
厨房では別の仕事を進められる
↓
お肉が焼き上がる
↓
その仕事の続きに戻る
↓
盛り付ける
というイメージです。
ここで大事なのは、
お肉を使う料理の続きが、そのまま進んでいるわけではない
ということです。
お肉が焼き上がるまでは、
その料理の続きは待っています。
ただし、
コックさん自身がオーブンの前を占領して待っている必要はない。
そのため厨房では、その間に別の仕事を進められます。
プログラムでも同じように、
awaitより後の処理は待っている。
でも、待っている間ずっとUIスレッドを占有しているわけではない。
と考えると分かりやすいと思います。
「awaitしているのに画面が止まらない」の正体
ここまでをまとめると、
await GetDataAsync();
では、
「GetDataAsyncが終わるまで何もかも止める」
わけではありません。
止まるのは、
そのメソッドのawaitより後の処理
です。
例えば、
StatusText.Text = "取得中...";
var result = await GetDataAsync();
StatusText.Text = result;
なら、
「取得中...」を表示
↓
GetDataAsync開始
↓
await
↓
このメソッドはいったん中断
↓
UIスレッドは他の仕事へ
↓
GetDataAsync完了
↓
このメソッドの続きへ
↓
結果を画面に表示
となります。
だから、
awaitしているのに画面全体は止まらない
わけです。
asyncを付ければ何でも画面が止まらなくなる?
ここは、もう一つ大事なポイントです。
答えは、
なりません。
例えば、
private async void Button_Click(object sender, RoutedEventArgs e)
{
HeavyCalculation();
await Task.Delay(1000);
}
とします。
HeavyCalculation()が非常に時間のかかる計算で、それをUIスレッド上で実行しているなら、
HeavyCalculationを実行している間は画面が固まる可能性があります。
つまり、
asyncを付ける
↓
全部自動的に非同期になる
ではありません。
async / awaitは魔法ではないんです。
特に、
・Web APIなどの通信
・ファイルアクセス
・データベースアクセス
・一定時間の待機
など、
結果を待つ時間が発生する処理
でasync / awaitはよく使われます。
一方、CPUを使い続ける重い計算は、単純にasyncを付けただけでは解決しません。
デバッグでは「asyncがあるか」ではなく流れを見る
例えば、
async / awaitを使っているのに画面が固まる
という問題が発生したとします。
そんなとき、
async
が付いているかだけを見ても、原因は分かりません。
例えば、
ボタン押下
↓
asyncメソッド開始
↓
重い処理
↓
await
↓
続きの処理
なら、
awaitより前にUIスレッドを止めている処理がないか?
を見る必要があります。
ほかにも、
Thread.Sleep(...)
のようにスレッドを直接止めていないか。
時間のかかる同期処理を呼び出していないか。
そうやって、
処理の流れを一つずつ追っていく
ことが大切です。
「asyncが付いているから非同期」
ではなく、
どこで待っている?
何を待っている?
その間、UIスレッドは何をしている?
と考える。
これができるようになると、非同期処理もかなり追いやすくなります。
まとめ
今回は、
C#のasync / await
について紹介しました。
覚えておきたいポイントは、
・asyncを付けただけで別スレッドになるわけではない
・awaitは非同期処理の完了を待つ
・awaitより後の処理は、完了するまで進まない
・ただし待っている間、スレッドをブロックし続けるわけではない
・Thread.Sleepとawait Task.Delayでは「待ち方」が違う
です。
最初は、
awaitって待つんだから、同期処理と同じじゃないの?
と思うかもしれません。
でも、
「処理が終わるのを待つ」ことと、
「スレッドを止めて待つ」ことは違う。
ここが分かると、async / awaitのコードも少し違って見えてくると思います。
関連記事
前回の記事
処理は順番に動くとは限らない?|新人・初級PG向け 同期・非同期の基本
プログラミングの仕組みから理解したい人へ
新人・初級PGが最初に覚えた方がいいこと|言語より先に理解したいプログラムの仕組み
処理の流れを追えるようになりたい人へ
ソースは読めるのに処理が追えないのはなぜ?|フローを追っていたはずなのに迷子になる原因
処理は追えるのに「なぜそれをしているのか」が分からない人へ | コードから目的を読み解く考え方