.NETにおける非同期処理の概要
非同期コードを導入する主なシナリオは以下の2つです:
- I/Oバウンド処理:ネットワークやディスクからのリソース取得を伴う操作
- CPUバウンド処理:メモリ内で実行される計算集約型の処理
本セクションでは、それぞれの処理タイプに対してasyncとawaitを使用した実際の例を紹介します。外部プロセスの完了待ちやアプリケーション内の計算集約型処理において、非同期コードを利用することでパフォーマンスを向上させることができます。
I/Oバウンド処理
ファイルやネットワーク操作に制限されるI/Oバウンドコードでは、操作完了時にasyncとawaitを使用して待機する必要があります。
.NETでネットワークやファイルI/Oを実行するメソッドは非同期対応済みであるため、Task.Runを使用する必要はありません。
例1:テキストファイル読み込み
public async Task<List<string>> ReadFileLinesAsync(string filePath)
{
using var fileReader = File.OpenText(filePath);
var content = await fileReader.ReadToEndAsync();
return content
.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
.ToList();
}
例2:Webからのデータ取得
public async Task<List<string>> FetchWebContentAsync(string url)
{
var client = new HttpClient();
var content = await client.GetStringAsync(url);
return content
.Split(new[] { Environment.NewLine }, StringSplitOptions.RemoveEmptyEntries)
.ToList();
}
CPUバウンド処理
このケースでは、アプリケーションは外部プロセスの完了を待たず、自身で時間のかかる計算処理を実行し、処理中にアプリケーションが応答性を保つ必要があります。
データモデル定義
[Serializable]
public class JournalRecord
{
public string Title { get; set; }
public string Description { get; set; }
public DateTime EntryDate { get; set; }
public string EntryText { get; set; }
}
XMLデシリアライズ処理
private List<JournalRecord> ParseXmlRecords(List<string> xmlData)
{
var records = new List<JournalRecord>();
var serializer = new XmlSerializer(typeof(JournalRecord));
foreach (var xml in xmlData)
{
if (xml == null) continue;
using var reader = new StringReader(xml);
var record = (JournalRecord)serializer.Deserialize(reader)!;
if (record == null) continue;
records.Add(record);
}
return records;
}
非同期ラッパーメソッド
public async Task<List<JournalRecord>> ParseRecordsAsync(List<string> xmlData)
{
return await Task.Run(() => ParseXmlRecords(xmlData));
}
JSONデシリアライズの比較
public List<JournalRecord> ParseJsonRecords(List<string> jsonData)
{
var records = new List<JournalRecord>();
foreach (var json in jsonData)
{
if (string.IsNullOrWhiteSpace(json)) continue;
records.Add(JsonSerializer.Deserialize<JournalRecord>(json)!);
}
return records;
}
public async Task<List<JournalRecord>> ParseJsonRecordsAsync(List<string> jsonData)
{
var records = new List<JournalRecord>();
foreach (var json in jsonData)
{
if (string.IsNullOrWhiteSpace(json)) continue;
using var stream = new MemoryStream(Encoding.Unicode.GetBytes(json));
records.Add((await JsonSerializer.DeserializeAsync<JournalRecord>(stream))!);
}
return records;
}
並列処理による最適化
public async Task<List<JournalRecord>> ProcessMultipleRecordsAsync(List<string> jsonData)
{
var tasks = jsonData.Select(ParseSingleRecordAsync);
return (await Task.WhenAll(tasks)).ToList();
}
private async Task<JournalRecord> ParseSingleRecordAsync(string json)
{
if (string.IsNullOrWhiteSpace(json))
return new JournalRecord();
using var stream = new MemoryStream(Encoding.Unicode.GetBytes(json));
return (await JsonSerializer.DeserializeAsync<JournalRecord>(stream))!;
}
ネストされた非同期処理
非同期メソッドを使用する際は、実行順序を維持するためにawaitの使用が重要です。また、現在のスレッドのエントリポイントへのawait呼び出しチェーンの維持も重要です。
コンソールアプリケーションでの実装例
public async Task ExecuteOperationsAsync()
{
Console.WriteLine($"処理開始 in {nameof(ExecuteOperationsAsync)}");
await FirstOperationAsync();
await SecondOperationAsync();
Console.WriteLine($"処理完了 in {nameof(ExecuteOperationsAsync)}");
}
private async Task FirstOperationAsync()
{
Console.WriteLine($"処理実行中 in {nameof(FirstOperationAsync)}");
await NestedOperationAsync();
Console.WriteLine($"処理終了 in {nameof(FirstOperationAsync)}");
}
private async Task SecondOperationAsync()
{
Console.WriteLine($"処理実行中 in {nameof(SecondOperationAsync)}");
await Task.Delay(500);
Console.WriteLine($"処理終了 in {nameof(SecondOperationAsync)}");
}
private async Task NestedOperationAsync()
{
Console.WriteLine($"処理実行中 in {nameof(NestedOperationAsync)}");
await Task.Delay(1500);
Console.WriteLine($"処理終了 in {nameof(NestedOperationAsync)}");
}
誤った実装例
public async Task IncorrectExecutionAsync()
{
Console.WriteLine($"処理開始 in {nameof(IncorrectExecutionAsync)}");
FirstOperationAsync(); // awaitがない
await SecondOperationAsync();
Console.WriteLine($"処理完了 in {nameof(IncorrectExecutionAsync)}");
}
public async Task BlockingExecutionAsync()
{
Console.WriteLine($"処理開始 in {nameof(BlockingExecutionAsync)}");
FirstOperationAsync().Wait(); // ブロッキング呼び出し
await SecondOperationAsync();
Console.WriteLine($"処理完了 in {nameof(BlockingExecutionAsync)}");
}
Taskオブジェクトの活用
既存プロジェクトにスレッドを導入する際、Taskオブジェクトを直接使用することは非常に有効です。asyncとawaitを導入する際には、呼び出しスタック全体を更新することが重要ですが、大規模なコードベースでは変更範囲が広くなり、多くの回帰テストが必要になります。
インターフェース定義例
public interface IAsyncProcessor
{
void ProcessOrders(List<Order> orders);
Task ProcessOrdersAsync(List<Order> orders);
List<Order> GetOrders(int customerId);
Task<List<Order>> GetOrdersAsync(int customerId);
}
Taskメソッドの使用例
public void HandleOrders(List<Order> orders, int customerId)
{
Task<List<Order>> processTask = Task.Run(() => PrepareOrders(orders));
Task labelTask = Task.Factory.StartNew(() => CreateLabels(orders), TaskCreationOptions.LongRunning);
Task sendTask = processTask.ContinueWith(task => SendOrders(task.Result));
Task.WaitAll(new[] { labelTask, sendTask });
SendConfirmation(customerId);
}
同期・非同期実行の切り替え
public void ProcessDataSet(object data, bool requiresUiThread)
{
Task processTask = new(() => DoDataProcessing(data));
if (requiresUiThread)
{
// 現在のスレッドで同期実行(UIスレッド想定)
processTask.RunSynchronously();
}
else
{
// バックグラウンドでThreadPoolスレッド上で実行
processTask.Start();
}
}
Taskプロパティの確認
StatusプロパティはTaskStatus列挙型を返し、以下の8つの値を取ります:
Created(0):タスク作成済みだがThreadPoolにスケジュールされていないWaitingForActivation(1):.NETによるスケジュール待ちWaitingToRun(2):スケジュール済みだが実行開始前Running(3):現在実行中WaitingForChildrenToComplete(4):タスク完了済みだが子タスクが実行中RanToCompletion(5):正常完了Canceled(6):キャンセル確認済みFaulted(7):未処理例外発生
状態確認用ショートカットプロパティ
IsCanceled:ステータスがCanceledの場合trueIsCompleted:ステータスがRanToCompletion、Canceled、Faultedの場合trueIsCompletedSuccessfully:ステータスがRanToCompletionの場合trueIsFaulted:ステータスがFaultedの場合true
例外処理例
Task ordersTask = Task.Run(() => ProcessOrders(orders, 123));
try
{
ordersTask.Wait();
Console.WriteLine($"タスクステータス: {ordersTask.Status}");
}
catch (AggregateException)
{
Console.WriteLine($"タスク例外! エラーメッセージ: {ordersTask.Exception.Message}");
}
同期コードとの相互運用
同期から非同期への呼び出し
public class DataProcessor
{
private ApiService _apiService;
public DataProcessor()
{
_apiService = new ApiService();
}
public RecordData? GetProcessedData(int recordId)
{
RecordData? data = null;
try
{
data = _apiService.FetchRecordAsync(recordId).Result;
}
catch (AggregateException ae)
{
Console.WriteLine($"データ読み込みエラー: {ae.Flatten().Message}");
}
if (data != null)
{
data = ProcessAdditionalInfo(data);
return data;
}
else
{
return null;
}
}
private RecordData ProcessAdditionalInfo(RecordData data)
{
// 追加処理
return data;
}
}
非同期から同期への呼び出し
public class AsyncDataProcessor
{
private DataService _dataService = new DataService();
private async Task<RecordData> EnhanceRecordDataAsync(RecordData data)
{
await Task.Delay(100);
// 追加処理
return data;
}
public async Task<RecordData?> GetEnhancedDataAsync(int recordId)
{
RecordData? data = null;
try
{
data = await Task.Run(() => _dataService.FetchRecord(recordId));
}
catch (Exception e)
{
Console.WriteLine($"データ読み込みエラー: {e.Message}");
}
if (data != null)
{
data = await EnhanceRecordDataAsync(data);
return data;
}
else
{
return null;
}
}
}
複数バックグラウンドタスクの処理
public async Task<Patient> LoadPatientConcurrentAsync(int patientId)
{
var tasks = new List<Task>
{
LoadPatientDetailsAsync(patientId),
LoadDoctorInfoAsync(patientId),
LoadMedicationsAsync(patientId)
};
await Task.WhenAll(tasks.ToArray());
_patient.Medications = _medications;
_patient.PrimaryCareProvider = _doctor;
return _patient;
}
public Patient LoadPatientSequential(int patientId)
{
var tasks = new List<Task>
{
LoadPatientDetailsAsync(patientId),
LoadDoctorInfoAsync(patientId),
LoadMedicationsAsync(patientId)
};
Task.WaitAll(tasks.ToArray());
_patient.Medications = _medications;
_patient.PrimaryCareProvider = _doctor;
return _patient;
}
非同期プログラミングのベストプラクティス
- 常に
asyncとawaitを優先し、同期メソッドやWait()、Resultなどのブロッキング呼び出しを避ける Task.WhenAllで複数操作を同時に待機する場合を除き、メソッドを直接awaitするasync voidを使用しない。戻り値はTask、Task<TResult>、ValueTask、またはValueTask<TResult>とする- ブロッキングコードと非同期コードを混在させない。呼び出しスタック全体で非同期呼び出しを使用する
- 追加引数を渡す必要がない限り、
Task.Factory.StartNewではなくTask.Runを使用する - 長時間実行される非同期メソッドはキャンセルをサポートする
- 共有データの同期使用を行う。ロックを追加してスレッド間で使用されるオブジェクトのデータが上書きされないようにする
- ネットワークやファイルアクセスなどのI/Oバウンド処理には常に
asyncとawaitを使用する - 非同期メソッドを作成する際は、名前にAsyncサフィックスを追加する
- 非同期メソッド内で
Thread.Sleepを使用しない。固定時間待機が必要な場合はawait Task.Delayを使用する