The following two tabs change content below.
アバター画像

メンバー

福岡市博多区でWebシステム開発の受託・ラボ型SES・web集客サービス、3つのWebサービスを提供中

前回の記事では「同期処理と非同期処理の違い」を概念的に解説しました。
今回はさらに一歩進んで、実際にコードを書きながら「体感」して理解していきましょう。

非同期処理について何ぞや。と思っている方はこちらの記事へ

実際にC#のコードを例に

同期処理の書き方

非同期処理の書き方

・async/awaitを使うとどう変わるか

をサンプルコード付きで比較していきます。

C#でasync/awaitを体感する

それでは早速、同期処理についてコード付きで解説していきます。

同期処理とは「順番に処理が流れる」というイメージでしたね。

まずはボタンを4つ置いたフォームを用意します。(Form1 など)

準備するそれぞれのボタンについて説明しておきます。
パターンA:btnSync_Click
     ⇒同期処理ボタン
パターンB:btnAsyncAwait_Click
     ⇒正しい非同期処理ボタン
パターンC:btnMissingAwait_Click
     ⇒await漏れボタン
パターンD:btnBlockingWait_Click
     ⇒.Wait()ボタン

下記のプログラムはForm1クラス内にパターンA~Dの挙動をそれぞれ4つのボタンに埋め込んでいます。

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        // ボタン: btnSync, btnAsyncAwait, btnMissingAwait, btnBlockingWait を配置想定
        btnSync.Click += btnSync_Click;				//同期処理ボタン
        btnAsyncAwait.Click += btnAsyncAwait_Click;	//正しい非同期処理ボタン
        btnMissingAwait.Click += btnMissingAwait_Click;	//await漏れボタン
        btnBlockingWait.Click += btnBlockingWait_Click;	//.Wait()ボタン
    }

    // 疑似的に重い処理(バックグラウンドで2秒かかる仕事)
    private async Task HeavyWorkAsync(string label)
    {
        await Task.Delay(2000);
        Console.WriteLine($"{label} 完了 (スレッドID: {Environment.CurrentManagedThreadId})");
    }
}

パターンA:同期処理(UIが完全にフリーズ

次のソースは同期処理のボタンが押下された想定となります。

Thread.Sleep でガッツリ止める(ブロッキング)処理を記載しています。

private void btnSync_Click(object? sender, EventArgs e)
{
    labelStatus.Text = "同期: 6秒待機中…(UIは固まります)";

    // 4秒×1回ブロック
    Thread.Sleep(4000); // 1回目
    Console.WriteLine("No1 完了 (4秒後)");

    // 2秒×1回ブロック
    Thread.Sleep(2000); // 2回目
    Console.WriteLine("No2 完了 (6秒後)");

    labelStatus.Text = "同期: 完了";
}

こちらのプログラムは、クリック直後から UI操作不能となります
(ドラッグ・クリック・再描画すべて固まる)

コンソールの出力結果は必ず

No1 完了 (4秒後)
No2 完了 (6秒後)

の順に出力されます。

パターンB:正しい非同期(async/await、UIは固まらない

次のソースは非同期処理のボタンが押下された想定となります。

await Task.Delay でノンブロッキングに待機する処理を記載しています。

private async void btnAsyncAwait_Click(object? sender, EventArgs e)
{
    labelStatus.Text = "正しい非同期: 実行中(UIは操作できます)";

    // 2秒×2回分ブロック
    await Task.Delay(3000); // No1
    Console.WriteLine("No1 完了 (3秒後)");

    await Task.Delay(2000); // No2
    Console.WriteLine("No2 完了 (5秒後)");

    labelStatus.Text = "正しい非同期: 完了";
}

こちらのプログラムは、先ほどと違いクリック直後も UIは固まらず、ボタンも押下可能です
(ボタン連打・ウィンドウ移動OK)

コンソールの出力結果は必ず

No1 完了 (3秒後)
No2 完了 (5秒後)

の順に出力されます。

ポイント

AとBはログの出力は似ているが、AはUIが固まりBはUIが固まらないのが決定的な違いとなります!

パターンC:await を忘れる(待たずに進む=タイミングずれ

次のパターンはコンパイルエラーは起きないですが、バグが起きる可能性のあるソース例です。

awaitを付け忘れてしまいTask.Delayとだけ記載することで、「非同期タスクを作っただけ」 となり即時に次の処理ヘ進んでしまいます。

private async void btnMissingAwait_Click(object? sender, EventArgs e)
{
    labelStatus.Text = "await忘れ: 実行中";

    // await を付け忘れ → タスクが「裏で」動き始めるが、ここでは待たない
    Task.Delay(6000); 
    Console.WriteLine("No1 完了(実は待ってない)");

    await Task.Delay(1000);
    Console.WriteLine("No2 完了(これは待つ)");

    labelStatus.Text = "await忘れ: 完了(でも裏で何か残ってるかも…)";
}

コンソールの出力結果は

No1 完了(実は待ってない)
No2 完了(これは待つ)

となります。

Task.Delay(6000) では6秒待たれずに即次に進みます。
コンソールの「No1 完了(実は待ってない)」はクリックされて0秒で出力されるイメージです。
(バグの温床というやつですね)

ポイント

コンパイルは通る」ので気づきにくい。順序前提の所でデータ未取得や二重実行が発生しやすい。

パターンD:.Wait() でブロック(UIが固まるデッドロックの危険

次のパターンは非同期処理を同期的に待つとどうなるか、といったパターンです。
(実際にこういった矛盾的なソースは書かないと思いますが。。)

private async void btnBlockingWait_Click(object? sender, EventArgs e)
{
    labelStatus.Text = ".Wait(): ブロッキング待機(UIが固まります)";

    // 実際の非推奨例:.Result や .Wait() は UI スレッドで使わない
    Task.Delay(2000).Wait(); // ← ここでUIが2秒フリーズ
    Console.WriteLine("No1 完了(ブロック後)");

    await Task.Delay(1000); // これはノンブロッキング
    Console.WriteLine("No2 完了");

    labelStatus.Text = ".Wait(): 完了";
}

コンソールの出力結果は

No1 完了 (2秒後。ブロック後)
No2 完了 (3秒後)

となります。

流れでいうと
①ボタンをクリック
②2秒間アプリが固まる(UIスレッドをブロックしているため)
③「No1 完了」表示
④await Task.Delay(1000) により1秒後に処理再開(この時UIは固まらない)
⑤「No2 完了」表示
といった感じです。

.Result / .WaitをUIスレッドで使用するとawaitの再開が同じスレッドで行われるため、相互で待ちあってデッドロックを起こす可能性があります。

基本的に非同期処理ではasync/awaitをセットで書くと思うので皆さんがそんなに気にすることではないと思いますが。。

ポイント

・UIスレッドでは .Wait() .Result を使用しない
・常に await で非同期のまま通す


4パターンの違い・早見表

ここまでつらつらと4つのパターンを用いて同期処理・非同期処について説明してきましたが、それぞれの違いの早見表を以下に記載しておきます。

パターン待ち方UIの挙動代表的な落とし穴
A:同期 (Thread.Sleep)ブロッキング固まる体験最悪、応答なし
B:正しい非同期 (await)ノンブロッキング固まらないー(正解パターン)
C:await忘れ待たない固まらない順序ズレ/未完了のまま進む
D:.Wait()(UIで)ブロッキング固まるデッドロック・フリーズ

まとめ

ここまでで、「同期処理」と「非同期処理」の違いを、実際のC#コードを通して体感できたと思います。

UIが固まってしまう「同期処理」と、裏でちゃんと動いてくれる「非同期処理」。

たった数行の違いでも、アプリの体感はまるで別物だと分かって頂けたと思います。

まさに「コードで“待つ”ことをどう扱うか」が非同期処理の肝なんです。

非同期処理の真の目的は、“処理を速くすること”ではありません。

「アプリを止めない」こと。

そして“ユーザーの操作を奪わない”ことなんです。

この考え方を理解しておくと、今後ネットワーク通信やファイル読み込み、APIリクエストなど「時間のかかる処理」にどう向き合えばいいかが見えてきます。

次回の第3回では、いよいよ実務での使いどころ設計の考え方に踏み込みます。

「どんな処理を非同期にすべきか」「async/awaitを安全に使うには」など、現場でよくあるパターンをもとに、より実践的な非同期処理を解説していきます。

それでは次の章でお会いしましょう!