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?

asyncを付けたのに速くならないのはなぜ?

0
Posted at

C#でasync / awaitを覚える。

なんとなく分かってきた。

「なるほど。時間のかかる処理は非同期にすればいいのか」

そして実際にコードを書いてみる。

var result = await GetDataAsync();

よし。

これで非同期になった。

きっと速くなる。

……。

あれ?

全然速くなってない。

処理時間を測ってみても、ほとんど変わらない。

なんでやねん(笑)

でも、これ。

別にasyncがサボっているわけではありません。

そもそもasync / awaitは、

処理そのものを速くするための仕組みではない

からです。

今回は、

「非同期にしたのに、なぜ速くならないの?」

を考えてみます。

また、コックさんに働いてもらいます

以前の記事でも登場してもらったコックさん。

今回も働いてもらいましょう(笑)。

コックさんがお湯を沸かしています。

お湯が沸くまで3分かかるとします。

同期処理なら

鍋を火にかける。

そして、その前で待つ。

「まだかなぁ……」

3分間、鍋の前で待ちます。

お湯が沸いた。

よし、次の仕事。

では、非同期ならどうなるでしょう。

非同期処理なら

鍋を火にかける。

でも、鍋の前では待ちません。

「沸いたら教えてね」

として、その場を離れる。

その間に食器を準備したり、ほかの仕事を進めたりする。

そして3分後。

お湯が沸いたので戻ってきて、続きを始めます。

おお。

なんだか効率が良さそうです。

では、ここで問題です。

お湯が沸くまで何分かかったでしょう?

……3分です(笑)。

非同期にしたからといって、

3分かかるお湯が1分で沸くわけではありません。

変わったのは、

コックさんが3分間ずっと鍋の前で待つ必要がなくなったこと。

ここが、

「非同期」と「高速化」

を考えるときの大事な違いです。

asyncにしたら、何が変わったの?

コードに戻ります。

var result = await GetDataAsync();

GetDataAsync()が、サーバーからデータを取得するのに3秒かかるとします。

ここにawaitを書いたからといって、

3秒 → 1秒

になるわけではありません。

サーバーから結果が返ってくるまで3秒かかるなら、結果を受け取れるのも約3秒後です。

じゃあ、

asyncにした意味ないじゃん。

……となりそうですが、もちろんそんなことはありません(笑)。

変わるのは、

その3秒の「待ち方」です。

同期的なI/Oでは、結果が返ってくるまで呼び出したスレッドがその場で待つことがあります。

一方、非同期I/Oをawaitして、その処理がまだ完了していない場合、awaitしているメソッドはいったん呼び出し元へ制御を返します。

そのため、

待っている間、呼び出し元のスレッドをブロックし続ける必要がありません。

少し難しくなりましたね(笑)。

WPFで考えるなら、もっと単純です。

UIスレッドが、

「通信が終わるまで、ここで3秒待ってます」

と立ち止まらなくてよくなる。

その間に、

・ボタン操作を受け付ける
・ウィンドウを移動する
・画面を再描画する
・ほかのイベントを処理する

といった、本来の仕事を続けられます。

つまり、

3秒の処理が1秒になったわけではない。

3秒待っている間、UIスレッドまで一緒に待たせなくてよくなった。

ということです。

asyncを書けば、いつでもこうなるわけではない

ここは少し注意が必要です。

async

と書けば、自動的に重い処理が別の場所へ移動して、UIスレッドが自由になるわけではありません。

async / awaitが力を発揮する代表的な場面は、

通信やファイルアクセスなど、非同期で完了を待てる処理

です。

逆に、UIスレッド上で大量の計算をしているだけなら、

private async Task CalculateAsync()
{
    HeavyCalculation();
}

asyncを付けただけでは、重い計算が勝手に別スレッドへ移動するわけではありません。

CPUは相変わらず、その計算をしています。

なので、

asyncを付けた = UIスレッドを使わなくなった

ではありません。

ここは分けて考える必要があります。

「画面が止まらない」と「処理が速い」は別の話

これ、体感だとちょっと紛らわしいんですよね。

ある処理に3秒かかっている。

同期的に実行すると、その3秒間画面が固まってしまう。

そこで適切な非同期処理に変えてみる。

すると、

画面が動く!

ボタンも反応する。

ウィンドウも動かせる。

なんとなく、

「速くなった!」

気がします。

でも測ってみると、

同期処理    約3秒
非同期処理  約3秒

あれ?

やっぱり速くなってない(笑)

でもユーザーからすると、

3秒間、完全に固まっているアプリ

と、

処理を待っている3秒間も反応してくれるアプリ

では、まったく違います。

だから非同期化にはちゃんと意味があります。

ただ、

「画面が固まらなくなった」=「処理時間が短くなった」

ではない。

ここは分けて考えた方がよさそうです。

じゃあ、本当に速くしたいときは?

ここまで来ると、次に思います。

「じゃあ、本当に処理を速くしたいときはどうするの?」

ですよね。

僕なら、まずここを考えます。

そもそも、何に時間がかかってるんだろう?

これが分からないと、

遅い!
↓
じゃあasync!

とやっても、

速くならないものは速くなりません(笑)

たとえば、大きく分けるとこんなケースがあります。

外からの結果を待っている

ネットワーク通信、ファイルアクセス、データベースアクセスなどです。

こうした処理では、自分のCPUがずっと計算しているわけではなく、

外部の処理が完了するのを待っている時間

があります。

利用しているAPIが非同期I/Oに対応していれば、awaitを使うことで、その待ち時間に呼び出し元のスレッドをブロックし続けずに済みます。

でも、

通信そのものが速くなったわけではありません。

さっきのお湯と同じです。

お湯が沸く時間ではなく、

待っている側の時間の使い方が変わった

ということです。

CPUが頑張っている

一方で、大量の計算や画像処理など、

CPU自身が一生懸命働いている処理

もあります。

こちらは、

async

と書いた瞬間、

CPUが、

「よし!今日はいつもの3倍頑張るぞ!」

となるわけではありません🤣

処理時間そのものを短くしたいなら、まず処理内容やアルゴリズムを見直す。

そのうえで処理の性質によっては、並列化など別の方法を検討することになります。

だから、

「遅いから、とりあえずasync」

ではなく、

「何が遅いんだろう?」

から考える。

僕はこっちの方が大事だと思っています。

でも、非同期処理で全体時間が短くなることもあるよね?

あります。

ここがまた、ややこしいところです(笑)。

たとえば、独立した二つの非同期処理があるとします。

var user = await GetUserAsync();
var product = await GetProductAsync();

どちらも3秒かかるとしましょう。

この書き方では、

まずGetUserAsync()を開始して、その完了を待ちます。

約3秒。

それが終わってからGetProductAsync()を開始して、また待ちます。

約3秒。

なので、全体ではざっくり6秒です。

でも、この二つがお互いに依存しておらず、同時に開始して問題ない処理なら、

var userTask = GetUserAsync();
var productTask = GetProductAsync();

await Task.WhenAll(userTask, productTask);

var user = await userTask;
var product = await productTask;

のように、二つの処理を先に開始してから、両方の完了を待つことができます。

両方とも約3秒で、待ち時間を重ねられるなら、全体の待ち時間は約3秒になる可能性があります。

ここで、

「ほら!asyncにしたら速くなった!」

……と言いたくなります(笑)。

でも、GetUserAsync()が3秒から1.5秒になったわけではありません。

GetProductAsync()も同じです。

一つ一つの処理時間は約3秒のまま。

変わったのは、

一つ終わるまで待ってから次を始めるのではなく、独立した二つを先に開始して、待ち時間を重ねたこと。

です。

このケースでは結果として、一連の処理全体が早く終わる可能性があります。

ただし、何でも同時に始めればいいわけではありません。

前の処理結果が次の処理に必要なら、当然同時には始められません。

また、サーバーやデータベースへの負荷、接続数、API側の制限なども考える必要があります。

Task.WhenAllにすれば何でも高速化!

という新しい魔法を作らないようにしましょう(笑)。

非同期と並列は、同じものではない

ここも一度整理しておきます。

async / awaitを覚えたころって、

なんとなく、

非同期
↓
同時に動く
↓
並列
↓
速い!

みたいなイメージを持ちやすい気がします。

でも、

非同期処理と並列処理は同じものではありません。

非同期処理では、

待っている間に、ほかの仕事を進められる

ことが重要です。

一方、並列処理は、

複数の処理を実際に並行して実行することで、処理時間の短縮などを狙う

考え方です。

async / awaitを書いたからといって、自動的に別スレッドで処理されたり、複数のCPUコアを使ったりするわけではありません。

今回のコックさんで言えば、

async / awaitは、

お湯を速く沸かす魔法ではありません。

お湯が沸くまでの3分間、

コックさんを鍋の前に縛り付けないための仕組み。

そんなふうに考えると、少し分かりやすいと思います。

まとめ

asyncを付けた。

awaitも使った。

よし。

これで速くなる!

……ならない(笑)。

でも、それでいいんです。

async / awaitは、

処理そのものを速くするための仕組みではありません。

大切なのは、

「何秒かかったか」だけではなく、その時間に何をしていたのかを見ること。

外部の処理が終わるのを待っていたのか。

CPUが一生懸命計算していたのか。

それとも、本当は同時に開始できる処理を一つずつ順番に待っていたのか。

そこを考えないまま、

遅い!
↓
asyncにしよう
↓
まだ遅い!
↓
Task.Runで囲もう
↓
……あれ?

となってしまうと、だんだん本当の原因から離れていきます(笑)。

だから、

「非同期にしたのに速くならない!」

と思ったら、一度こう考えてみる。

「そもそも僕は、何を速くしたかったんだろう?」

処理そのものにかかる時間を短くしたかったのか。

画面を固めたくなかったのか。

待っている間に別の仕事を進められるようにしたかったのか。

独立した待ち時間を重ねて、全体の完了を早めたかったのか。

同じ「遅い」でも、目的によってやることは変わります。

async / awaitを覚えることも大切ですが、

まず「何に時間がかかっているのか」を見る。

僕は、こっちの方が大切なんじゃないかなと思っています。

これまでの記事

async / awaitについて、これまでこんな疑問を順番に見てきました。

非同期処理を書いたのに画面が固まるのはなぜ?
→ async / awaitを使っても画面が固まるケースについて

awaitは待つのに、なぜ画面は止まらないの?
→ awaitの「待つ」とは何なのか

Task.Runを使えば画面は固まらない? ― とりあえずTask.Runが危ない理由
→ 「とりあえずTask.Run」で解決しようとすると何が起きるのか

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?