C#で非同期処理を理解しようとすると、途中で必ず「スレッドとは何か」という話に戻ってきます。async / await はよく「別スレッドで動く仕組み」だと誤解されますが、実際にはそれだけでは説明できません。まずは土台として、スレッドそのものの意味を整理しておきます。
スレッドは、一言でいえば処理が実行される流れの単位です。プロセスが「アプリケーション全体の入れ物」だとすると、スレッドはその中で実際に命令を順に実行していく作業単位です。ひとつのプロセスの中に複数のスレッドが存在することも珍しくありません。
ここで最初に修正しておきたい誤解があります。コンソールアプリのコードを見ると、上から下へ1本の流れで実行されているように見えます。そのため「1プロセス=1スレッド」という印象を持ちやすいのですが、実際にはそう単純ではありません。アプリケーションの中ではランタイムやOSが裏でさまざまな処理を管理しており、ThreadPoolなどの仕組みも存在します。見た目がシリアルであることと、実際の実行基盤が単一スレッドであることは別の話です。
プロセスとスレッド
まず、用語を簡単に切り分けます。
| 用語 | ざっくりした意味 |
|---|---|
| プロセス | 実行中のアプリケーション全体 |
| スレッド | その中で実際に処理を進める流れ |
プロセスの中には、メモリ空間や各種リソースがぶら下がっています。そしてスレッドは、そのプロセスの中で命令を実行します。つまり、プロセスが「作業場所」で、スレッドが「実際に手を動かす人」に近い関係です。
スレッドは「処理の器」
スレッドは、何か特別な高機能オブジェクトというより、処理を進めるための器だと考えると分かりやすいです。同期処理でも非同期処理でも、何かが実行される以上、どこかのスレッドがその処理を担当しています。
たとえば次のコードを考えます。
Console.WriteLine("A");
Thread.Sleep(3000);
Console.WriteLine("B");
この場合、同じスレッドが A を出力し、3秒停止し、その後 B を出力します。Thread.Sleep の間、そのスレッドは何もしないのに占有されたままです。これが同期処理の典型です。
同期処理とは何か
同期処理では、呼び出し元は処理が終わるまで先へ進めません。言い換えると、そのスレッドは待っている間も拘束されるということです。
同期処理の特徴
- 呼び出した側は完了まで待つ
- 待ち時間の間もスレッドを使い続ける
- 書き方は単純だが、待機時間が長いと効率が悪い
ここで重要なのは、「遅い」の本質です。同期処理が遅いというより、待っている間もスレッドを占有することが問題になります。I/O待ちが多い処理では、この点が特に効いてきます。
非同期処理とスレッドの関係
非同期処理を理解するときにまず押さえたいのは、async / await は「スレッドを増やす機能」ではない、ということです。より正確にいうと、待ち時間をスレッドに持たせないための仕組みです。
たとえば、
await Task.Delay(3000);
というコードでは、3秒間ずっとスレッドが待機しているわけではありません。await に到達すると、その時点で「続き」を記録し、今使っていたスレッドを手放します。そして3秒後、Task が完了したタイミングで続きが再開されます。
このときの本質は、処理が止まっている間、スレッドが遊ばないことです。
よくある誤解
ここで、非同期とスレッドに関する誤解を整理しておきます。
誤解しやすい点
asyncを付けると別スレッドで動くawaitを使うと並列処理になる- 非同期処理は必ず高速になる
これらはどれも半分だけ合っていて、半分は違います。
■ 実際には
asyncは別スレッドを作る命令ではないawaitは「中断して後で再開する」ための記法- 非同期は処理時間を短くするというより、スレッドの使い方を改善する
つまり、非同期処理のメリットは「1件の処理が速くなる」ことより、「待ち時間でスレッドを浪費しない」ことにあります。
コンソールアプリでも非同期は意味がある
「UIがないコンソールアプリで、待ち時間中にやることがないなら非同期の意味はあるのか」という疑問はもっともです。実際、やることが何もない場面もあります。ただ、それでも意味はあります。
同期処理では、待機中のスレッドはずっと拘束されます。非同期処理では、そのスレッドを返却できます。その返却されたスレッドは、ランタイムやThreadPoolによって別の処理に再利用される可能性があります。つまり、アプリ自身に他の仕事がなくても、実行基盤全体としては無駄が減るわけです。
まとめ
スレッドは、アプリケーションの中で処理を実行するための流れの単位です。プロセスがアプリ全体の入れ物であるのに対して、スレッドは実際に命令を順に進める役割を担います。
重要なのは、async / await を理解する前に、スレッドを「並列処理の特別な概念」としてではなく、処理を実行するための器として捉えることです。同期処理はその器を待ち時間中も占有し、非同期処理は待ち時間中にその器を手放します。この違いが、後でUIスレッドや async / await の理解につながっていきます。