0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

awaitは待つのに、なぜ画面は止まらないの?|C#のasync / awaitを理解しよう

0
Posted at

前回の記事では、

同期処理と非同期処理の違い

について紹介しました。

簡単に振り返ると、同期処理は、

処理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が最初に覚えた方がいいこと|言語より先に理解したいプログラムの仕組み

処理の流れを追えるようになりたい人へ

ソースは読めるのに処理が追えないのはなぜ?|フローを追っていたはずなのに迷子になる原因
処理は追えるのに「なぜそれをしているのか」が分からない人へ | コードから目的を読み解く考え方

デバッグで迷子になりやすい人へ

不具合は分かるのに、何を調べればいいか分からない人へ

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?