ファイルへの非同期アクセス (C#)
ファイルにアクセスする際に非同期機能を使用できます。 非同期機能を使用すると、コールバックの使用や複数のメソッドまたはラムダ式へのコードの分割を行わずに、非同期メソッドを呼び出すことができます。 同期コードを非同期コードにするには、同期メソッドの代わりに非同期メソッドを呼び出して、コードにいくつかのキーワードを追加するだけで済みます。
ファイル アクセスの呼び出しに非同期性を適用する利点には、次のようなものがあります。
- 非同期性により、UI アプリケーションの応答性が向上します。非同期処理を開始した UI スレッドが他の処理を実行できるためです。 UI スレッドが、時間のかかるコード、たとえば 50 ミリ秒を超えるコードを実行する必要がある場合、I/O が完了して、UI スレッドがキーボードやマウス入力などのイベントを再度処理できるようになるまで、UI が停止することがあります。
- 非同期性を適用すると、スレッドの必要性が軽減され、ASP.NET などのサーバー ベースのアプリケーションのスケーラビリティが向上します。 アプリケーションが応答ごとに専用スレッドを使用している場合、1,000 個の要求を同時に処理するには、1,000 個のスレッドが必要です。 非同期操作では、待機中にスレッドを使用する必要はほとんどありません。 既存の I/O 完了スレッドが最後に少しだけ使用されます。
- 現状ではファイル アクセス操作の待機時間が非常に短くても、将来に大幅に長くなる可能性があります。 たとえば、地球の裏側にあるサーバーにファイルが移動される場合があります。
- 非同期機能の使用に伴うオーバーヘッドはわずかです。
- 非同期タスクは簡単に並列実行できます。
適切なクラスを使用する
このトピックの簡単な例では、File.WriteAllTextAsync と File.ReadAllTextAsync について説明します。 ファイル I/O 操作を微調整するには、FileStream クラスを使用します。これには、オペレーティング システムのレベルで非同期 I/O を発生させるオプションがあります。 このオプションを使用することにより、多くの場合、スレッド プール スレッドがブロックされるのを回避できます。 このオプションを有効にするには、コンストラクター呼び出しで useAsync=true
または options=FileOptions.Asynchronous
引数を指定します。
ファイル パスを指定して StreamReader と StreamWriter を直接開いた場合、それらでこのオプションを使用することはできません。 一方、FileStream クラスによって開かれた Stream を使用する場合は、このオプションを使用できます。 UI アプリでは、スレッド プール スレッドがブロックされても、非同期呼び出しは高速になります。これは、UI スレッドは待機中にブロックされないためです。
テキストを書き込む
次の例では、ファイルにテキストを書き込みます。 各 await ステートメントに達すると、メソッドは直ちに終了します。 ファイル I/O が完了すると、メソッドは await ステートメントの後のステートメントから再開します。 async 修飾子は、await ステートメントを使用するメソッドの定義に含まれます。
簡単な例
public async Task SimpleWriteAsync()
{
string filePath = "simple.txt";
string text = $"Hello World";
await File.WriteAllTextAsync(filePath, text);
}
有限制御の例
public async Task ProcessWriteAsync()
{
string filePath = "temp.txt";
string text = $"Hello World{Environment.NewLine}";
await WriteTextAsync(filePath, text);
}
async Task WriteTextAsync(string filePath, string text)
{
byte[] encodedText = Encoding.Unicode.GetBytes(text);
using var sourceStream =
new FileStream(
filePath,
FileMode.Create, FileAccess.Write, FileShare.None,
bufferSize: 4096, useAsync: true);
await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
}
元の例には await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
ステートメントがあります。これは、次の 2 つのステートメントの省略形です。
Task theTask = sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
await theTask;
最初のステートメントはタスクを返し、ファイル処理を開始します。 await が含まれた 2 番目のステートメントによって、メソッドが直ちに終了し、別のタスクを返します。 ファイル処理が完了すると、await の後のステートメントに実行が戻ります。
テキストを読み取る
次の例では、ファイルからテキストを読み取ります。
簡単な例
public async Task SimpleReadAsync()
{
string filePath = "simple.txt";
string text = await File.ReadAllTextAsync(filePath);
Console.WriteLine(text);
}
有限制御の例
テキストはバッファーに格納されます。この例では StringBuilder に配置されます。 前の例と異なり、await の評価で値が生成されます。 ReadAsync メソッドによって Task<Int32> が返されます。処理の完了後、await の評価によって Int32
値 numRead
が生成されます。 詳しくは、「非同期の戻り値の型 (C#)」をご覧ください。
public async Task ProcessReadAsync()
{
try
{
string filePath = "temp.txt";
if (File.Exists(filePath) != false)
{
string text = await ReadTextAsync(filePath);
Console.WriteLine(text);
}
else
{
Console.WriteLine($"file not found: {filePath}");
}
}
catch (Exception ex)
{
Console.WriteLine(ex.Message);
}
}
async Task<string> ReadTextAsync(string filePath)
{
using var sourceStream =
new FileStream(
filePath,
FileMode.Open, FileAccess.Read, FileShare.Read,
bufferSize: 4096, useAsync: true);
var sb = new StringBuilder();
byte[] buffer = new byte[0x1000];
int numRead;
while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
{
string text = Encoding.Unicode.GetString(buffer, 0, numRead);
sb.Append(text);
}
return sb.ToString();
}
並列非同期 I/O
次の例では、10 個のテキスト ファイルを記述する並列処理を示します。
簡単な例
public async Task SimpleParallelWriteAsync()
{
string folder = Directory.CreateDirectory("tempfolder").Name;
IList<Task> writeTaskList = new List<Task>();
for (int index = 11; index <= 20; ++ index)
{
string fileName = $"file-{index:00}.txt";
string filePath = $"{folder}/{fileName}";
string text = $"In file {index}{Environment.NewLine}";
writeTaskList.Add(File.WriteAllTextAsync(filePath, text));
}
await Task.WhenAll(writeTaskList);
}
有限制御の例
WriteAsync メソッドは、ファイルごとにタスクを返します。タスクはタスクの一覧に追加されます。 await Task.WhenAll(tasks);
ステートメントはメソッドを終了し、すべてのタスクのファイル処理が完了すると、メソッド内で再開します。
この例では、タスクの完了後、finally
ブロックのすべての FileStream インスタンスを閉じます。 using
ステートメントで FileStream
が作成された場合は、タスクが完了する前に FileStream
が破棄されることがあります。
パフォーマンスの向上の多くは、非同期処理ではなく並列処理によって実現されます。 非同期性の利点は、複数のスレッドやユーザー インターフェイス スレッドが拘束されない点にあります。
public async Task ProcessMultipleWritesAsync()
{
IList<FileStream> sourceStreams = new List<FileStream>();
try
{
string folder = Directory.CreateDirectory("tempfolder").Name;
IList<Task> writeTaskList = new List<Task>();
for (int index = 1; index <= 10; ++ index)
{
string fileName = $"file-{index:00}.txt";
string filePath = $"{folder}/{fileName}";
string text = $"In file {index}{Environment.NewLine}";
byte[] encodedText = Encoding.Unicode.GetBytes(text);
var sourceStream =
new FileStream(
filePath,
FileMode.Create, FileAccess.Write, FileShare.None,
bufferSize: 4096, useAsync: true);
Task writeTask = sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
sourceStreams.Add(sourceStream);
writeTaskList.Add(writeTask);
}
await Task.WhenAll(writeTaskList);
}
finally
{
foreach (FileStream sourceStream in sourceStreams)
{
sourceStream.Close();
}
}
}
WriteAsync メソッドと ReadAsync メソッドを使用すると、CancellationToken を指定して、途中で処理をキャンセルすることができます。 詳細については、「マネージド スレッドのキャンセル」を参照してください。
関連項目
.NET