同期実行のボトルネックと非同期デリゲートの役割
システム開発において、長時間実行される処理を同期的に呼び出すと、呼び出し元のスレッドがブロックされ、アプリケーション全体の実行パフォーマンスが低下する問題が発生します。例えば、外部APIとの通信や大規模なデータ処理をそのまま同期呼び出しすると、UIスレッドがフリーズしたり、後続の業務フローが中断されたりします。この並行実行の課題を解決するため、.NET ランタイムでは「デリゲート(Delegate)」を使用した従来の非同期呼び出しパターンが用意されています。
標準的な同期メソッドを実装した場合、処理完了まで制御が返らないため並列作業が不可能です。以下のクラス例では、温度上昇をシミュレートする長時間実行処理を定義しています。
using System;
using System.Threading;
namespace AsyncDemo
{
public class ThermalUnit
{
public int TargetTemperature { get; set; }
private int _activeTemp;
public int ActiveTemp => _activeTemp;
public bool HeatingFinished { get; private set; }
public ThermalUnit() => HeatingFinished = false;
// 温度上昇をシミュレートする長時間実行メソッド
public int HeatUp(int initialTemp)
{
for (int temp = initialTemp; temp <= TargetTemperature; temp++)
{
Thread.Sleep(50); // 負荷を模擬
_activeTemp = temp;
Console.WriteLine($"現在温度: {_activeTemp}°C");
if (_activeTemp >= TargetTemperature)
{
HeatingFinished = true;
break;
}
}
return _activeTemp;
}
}
}
上記のような処理を直接呼び出すと、呼び出し元は完了まで待機状態になります。C#の型システムは、任意のメソッドに対して非同期実行をサポートするための自動生成パターンを提供しています。それがデリゲートの BeginInvoke と EndInvoke です。
コンパイラはデリゲート宣言に対し自動的に開始用メソッドと終了用メソッドを生成します。BeginInvoke は即座に制御権を返すため、メインスレッドは他のタスクを進められます。完了時の通知には AsyncCallback を使用し、コンテキスト情報は object パラメータで渡せます。
非同期実行を管理するラッパー実装
デリゲートを適切にカプセル化し、非同期呼び出しのライフサイクルを管理するサンプルクラスを示します。
using System;
using System.Threading;
namespace AsyncDemo
{
public class ProcessManager
{
private readonly ThermalUnit _unit;
// メソッドと同じ署名を持つデリゲートを定義
public delegate int HeatUpDelegate(int startTemp);
private readonly HeatUpDelegate _heatDelegate;
public ProcessManager(ThermalUnit unit)
{
_unit = unit;
_heatDelegate = new HeatUpDelegate(unit.HeatUp);
}
// 非同期呼び出しを開始
public IAsyncResult StartHeatingAsync(int startTemp, AsyncCallback callback, object state)
{
Console.WriteLine("[Main] 加熱プロセスを開始しました。");
return _heatDelegate.BeginInvoke(startTemp, callback, state);
}
// 非同期呼び出しの結果を取得(完了しない場合はブロッキング)
public int GetResultAsync(IAsyncResult result)
{
if (result == null) throw new ArgumentNullException(nameof(result));
try
{
return _heatDelegate.EndInvoke(result);
}
catch (Exception ex)
{
Console.WriteLine($"エラー発生: {ex.Message}");
throw;
}
}
public void ExecuteSideTask()
{
Console.WriteLine("[Main] サブモニタリングタスクを実行中...");
}
public void NotifyCompletion(IAsyncResult ar)
{
string contextInfo = ar?.AsyncState as string ?? "不明";
Console.WriteLine($"[Callback] 加熱完了。ステータス情報: {contextInfo}");
}
}
}
IAsyncResult を介した進捗制御パターン
BeginInvoke から返される IAsyncResult インターフェースは、非同期操作の状態管理を担当します。主なメンバーとして、ユーザー定義データを受け取る AsyncState、完了判定用の IsCompleted、およびスレッド同期用に設計された AsyncWaitHandle が挙げられます。
パターン1:ポーリングによる進行確認
ループ内で IsCompleted をチェックする方式です。実装が単純ですが、頻繁な判定によりCPUリソースを消費する傾向があるため、適切な遅延処理を組み込む必要があります。
public static void DemoPolling()
{
var device = new ThermalUnit { TargetTemperature = 80 };
var manager = new ProcessManager(device);
IAsyncResult token = manager.StartHeatingAsync(20, manager.NotifyCompletion, "監視モードA");
manager.ExecuteSideTask(); // コールバックより先に実行される
Console.Write("完了待ち... ");
while (!token.IsCompleted)
{
Console.Write("*");
Thread.Sleep(100); // CPU負荷軽減のためのスリープ挿入
}
int finalTemp = manager.GetResultAsync(token);
Console.WriteLine($"\n最終温度達成: {finalTemp}°C");
}
パターン2:WaitHandle によるブロッキング待ち
ポーリングよりもリソース効率が良いのが、AsyncWaitHandle を介したOSレベルのシグナル待ちです。これにより、指定ミリ秒までのタイムアウト設定も可能になり、無限ループや応答不良を防止できます。
public static void DemoBlockingWait()
{
var device = new ThermalUnit { TargetTemperature = 90 };
var manager = new ProcessManager(device);
IAsyncResult token = manager.StartHeatingAsync(30, null, "監視モードB");
// タイムアウト付きでブロッキング待機(1.5秒間)
bool signaled = token.AsyncWaitHandle.WaitOne(1500);
if (signaled)
{
Console.WriteLine("[Main] 待機中に処理が完了しました。");
int result = manager.GetResultAsync(token);
Console.WriteLine($"結果取得: {result}°C");
}
else
{
Console.WriteLine("[Main] タイムアウトしました。続行します。");
}
manager.ExecuteSideTask();
}
WaitOne に時間制限を設けることで、必要なタイミングで結果を取り出す制御が可能になります。デリゲートベースの非同期パターンは現代の開発では async/await や Task Parallel Library が主流ですが、コールバックチェーンやスレッド同期の基本構造を理解する上で重要な土台となっています。