C#のASP.NETで非同期処理を完全攻略!async/awaitの基礎から実践まで初心者向け解説
生徒
「C#を使ってWebアプリを作っているのですが、複数のユーザーから同時にアクセスがあったときに、画面の動きが重くなったり止まったりしてしまいます。何か良い解決方法はありますか?」
先生
「それはWebアプリ開発でとてもよくある課題ですね。C#のASP.NETでは、非同期処理という技術を使うことで、サーバーの負荷を減らし、たくさんのリクエストを同時に効率よく処理できるようになりますよ。」
生徒
「非同期処理という言葉は聞いたことがありますが、難しそうで具体的な実装方法がわかりません。初心者でも書けるようになりますか?」
先生
「大丈夫です!C#にはasyncとawaitという非常に便利な仕組みが用意されているので、基本さえ押さえれば誰でも簡単に書くことができます。それでは、基本的な使い方と仕組みを順番に見ていきましょう!」
1. 同期処理と非同期処理の違いとは?
プログラミングを学ぶ上で、処理の進み方には大きく分けて同期処理と非同期処理の二種類があります。パソコンを触ったことがない方でもイメージしやすいように、日常の作業に例えて説明します。
まず同期処理とは、一つの作業が完全に終わるまで、次の作業を一切始めずにじっと待つ方式のことです。例えば、レストランの厨房で料理人がお湯を沸かし始め、そのお湯が沸騰するまでの数分間、他の食材を切ることもせず、ただコンロの前で直立不動で待ち続けているような状態です。これでは時間がもったいないですし、効率が非常に悪いですよね。
一方で非同期処理とは、ある作業の待ち時間を利用して、別の作業を並行して進める方式のことです。先ほどの例で言えば、お湯を沸かしている間に野菜を切ったり、お皿を準備したりする動き方です。お湯が沸いたらその通知を受けて、元の作業に戻ります。このように時間を有効活用する仕組みが非同期処理です。
Webアプリ開発において、データベースからデータを読み込んだり、インターネット経由で別のサーバーと通信したりするときには、必ずわずかな待ち時間が発生します。この待ち時間にサーバーの頭脳を完全に停止させず、別のユーザーからのアクセスを処理できるようにするために、非同期処理が必須となります。
2. なぜASP.NETのWebアプリ開発で非同期処理が必要なのか
パソコンやサーバーの中では、スレッドと呼ばれる作業員が働いています。従来の同期処理でWebアプリを作ると、ユーザーからアクセスがあるたびに一人の作業員が専属で割り当てられます。その作業員がデータベースの処理などで待ち状態になると、その間は他の仕事ができなくなってしまいます。
もし同時に大量のユーザーがホームページにアクセスしてきた場合、作業員の数が足りなくなり、新しく来たユーザーは前の人の処理が終わるまで延々と待たされることになります。これが、ウェブサイトが重くなったり、エラー画面が出たりする原因です。
ここで非同期処理を導入すると、作業員はデータベースの返事待ちになった瞬間、その仕事を一度手放して、別のユーザーのアクセス処理へ手伝いに行くことができるようになります。そして、データベースの準備が整ったら、手の空いている別の作業員がその処理を引き継いで再開します。限られた人数の作業員で、何倍もの仕事を効率よくこなせるようになるため、ASP.NETでのWebアプリ開発では非同期処理の実装が強く推奨されています。
3. 非同期処理の基本となるasyncとawaitの使い方
C#で非同期処理を実装するときは、async(エイシンク)とawait(アウェイト)という二つのキーワードをセットで使用します。これは魔法の言葉のようなもので、プログラムの書き方を劇的にシンプルにしてくれます。
使い方の規則はとても単純です。まず、非同期で行いたい処理を含んだ部品の前に「async」と書きます。これにより、この部品は非同期処理を行う特別なものであるとコンピューターが認識します。そして、その部品の内部で、時間がかかる処理を呼び出す手前に「await」と書きます。これは「ここで待ち時間が発生するので、結果が出るまで作業員は一度解散して別の仕事に行ってください」という合図になります。
この二つは必ずペアで使う必要があり、awaitを使う場合は、その処理を囲んでいる一番外側の部品にasyncが付いていなければならないというルールがあります。この規則を覚えることが、C#の非同期プログラミングの第一歩です。
4. 非同期処理で使われるTask型とは何か
非同期処理を学ぶ上で避けて通れないのがTask(タスク)という型です。通常のプログラミングでは、文字を表す型や数値を表す型などがありますが、Task型は「将来終わる予定の仕事」そのものを表す特殊なデータ型です。
例えば、出前でラーメンを注文したとき、手元に届くのはラーメンそのものではなく、まずは注文票や引換券のようなものです。この引換券を持っていれば、後から本物のラーメンを受け取ることができます。C#の非同期処理におけるTask型は、まさにこの引換券の役割を果たしています。
値を返さない処理の場合はTaskを使い、最終的に何らかのデータ(例えば文字列や数値など)を返してほしい場合はTask<string>やTask<int>というように、引き換えたいデータの種類を後ろに書き込みます。awaitというキーワードは、この引換券を出して、本物のデータが届くまで待機するという指示を意味しています。
5. 初めての非同期処理!シンプルなコンソールプログラム
それでは、具体的なプログラムのコードを見ていきましょう。まずはWebアプリではなく、動きが理解しやすいシンプルな基礎コードから確認します。以下のコードは、疑似的に3秒間の待ち時間を発生させて、その間に処理が止まらないようにする簡単なプログラムです。
using System;
using System.Threading.Tasks;
class Program
{
static async Task Main(string[] args)
{
Console.WriteLine("処理を開始します。");
// 3秒間、非同期で待機する処理を呼び出します
await Task.Delay(3000);
Console.WriteLine("3秒経ちました。処理を終了します。");
}
}
上記のプログラムを実行すると、最初に行が出力された後、コンピューターは完全にフリーズすることなく、3秒間の待ち時間を非同期で過ごします。時間が経過すると、自動的に次の行の文字が画面に表示されます。
処理を開始します。
(3秒間の待ち時間)
3秒経ちました。処理を終了します。
ここで使用しているTask.Delay(3000)は、指定したミリ秒数(1000ミリ秒で1秒)だけ非同期のまま作業を一時停止させる命令です。従来の同期処理で使われていた命令とは異なり、スレッドという作業員を完全に拘束しないため、効率的な待機が可能になります。
6. ASP.NETのコントローラーでの非同期処理の実装パターン
次に、実際のWebアプリ開発の現場を想定して、ASP.NETのコントローラーと呼ばれる部分で非同期処理を記述するパターンを見てみましょう。ウェブサイトからボタンが押されたときに、データベースへユーザーの情報を保存しにいくという架空の処理を想定しています。
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
public class UserController : Controller
{
// 非同期処理を行うため、asyncとTaskを割り当てています
public async Task<IActionResult> RegisterUser()
{
// データの登録処理を非同期で実行し、終わるまで待機します
await SaveToDatabaseAsync();
// 登録が完了したら、画面にサクセスメッセージを表示します
return View("Success");
}
private async Task SaveToDatabaseAsync()
{
// データベース通信の代わりに、2秒間の疑似的な待ち時間を入れます
await Task.Delay(2000);
}
}
このコードでは、画面を制御するRegisterUserという部品の前にasync Task<IActionResult>が指定されています。これによって、このアクションメソッド全体が非同期に対応していることを表します。内部で呼び出しているSaveToDatabaseAsyncという部品の頭にもasyncが付いており、呼び出す際にawaitを記述することで、データベースへの書き込みを安全かつ効率的に待ち合わせることができます。
7. HTML画面にデータを表示するASP.NETの非同期処理
Webアプリでは、インターネットを通じて外部のサーバーから最新のニュースや天気予報などのデータを取得して、それを画面のHTMLに埋め込んでユーザーに見せるという処理がよく行われます。次は、外部から文字列データを非同期で引っ張ってきて、それをビューに渡すプログラムの例です。
using Microsoft.AspNetCore.Mvc;
using System.Threading.Tasks;
using System.Net.Http;
public class WeatherController : Controller
{
// 外部と通信するための道具を準備します
private static readonly HttpClient client = new HttpClient();
public async Task<IActionResult> ShowWeather()
{
// 外部のウェブサイトからデータを非同期でダウンロードします
string weatherData = await client.GetStringAsync("https://example.com/api/weather");
// 取得した文字データを画面の部品に詰め込んで送ります
ViewBag.Message = weatherData;
return View();
}
}
上記のプログラムでは、HttpClientという通信用の道具に備わっているGetStringAsyncという非同期の命令を呼び出しています。インターネットの通信は、相手のサーバーの状況によって時間がかかる場合が多いため、このようにawaitを使って非同期で処理を行うことが鉄則となっています。これにより、通信がどれだけ遅くても、自分のWebアプリのサーバーが巻き添えを喰らって停止する事態を防ぐことができます。
8. 複数の非同期処理を同時に実行して時間を短縮する方法
非同期処理のさらに強力なメリットとして、複数の時間がかかる作業をバラバラに同時スタートさせて、それらが全部終わるのをまとめて待つという使い方が挙げられます。例えば、一つの画面を作るために、プロフィール情報と、過去の投稿履歴の二つのデータを異なる場所から取得しなければならないケースです。
using System;
using System.Threading.Tasks;
class MultiTaskProgram
{
static async Task Main()
{
Console.WriteLine("データの読み込みを開始します。");
// 二つの作業を同時にスタートさせます(ここではまだawaitを付けません)
Task<string> userTask = GetProfileAsync();
Task<string> logTask = GetLogsAsync();
// 両方の引換券が出揃うのを、同時に待ち合わせます
await Task.WhenAll(userTask, logTask);
// 両方終わっているので、結果を安全に取り出すことができます
string userData = userTask.Result;
string logData = logTask.Result;
Console.WriteLine("すべてのデータが揃いました。");
}
static async Task<string> GetProfileAsync()
{
await Task.Delay(1500);
return "ユーザー情報";
}
static async Task<string> GetLogsAsync()
{
await Task.Delay(2000);
return "履歴データ";
}
}
もしこれを一つずつ順番に処理していたら、1.5秒足す2秒で、合計3.5秒の時間がかかってしまいます。しかし、このプログラムのように最初に二つの処理を同時に走らせて、Task.WhenAllという命令で一括して待ち合わせることで、一番時間の長い処理に合わせたわずか2秒で全ての作業が完了します。Webアプリの表示速度を極限まで高めるために、このテクニックは非常に重宝されます。
9. 初心者が非同期処理の実装で注意すべき落とし穴
非同期処理はとても強力ですが、未経験者や初心者が最も間違いやすい注意点がいくつかあります。これを正しく理解しておかないと、プログラムが途中で完全に固まって動かなくなるデッドロックという恐ろしい現象を引き起こす原因になります。
一番やってはいけない間違いは、非同期の引換券であるTask型を受け取っているにもかかわらず、awaitを使わずに、無理やり中身をこじ開けてデータを取り出そうとすることです。具体的には、プログラムの中で.Resultや.Wait()という命令を、asyncではない通常の処理の中で強引に呼び出す行為です。これを行うと、作業員同士が互いに相手の終了を待ち続ける無限ループに陥り、Webアプリ全体が応答なしになってしまいます。
非同期処理の世界に入ったら、上から下まで全ての通り道を非同期で統一するのが鉄則です。この原則は「非同期の連鎖」とも呼ばれ、呼び出し元の部品から呼び出される末端の処理まで、しっかりとasyncとawaitのキーワードを繋いでいくことが、バグのない安定したWebアプリを作るための最大の秘訣です。