ファイルへの非同期アクセス (C#)

ファイルにアクセスする際に非同期機能を使用できます。 非同期機能を使用すると、コールバックの使用や複数のメソッドまたはラムダ式へのコードの分割を行わずに、非同期メソッドを呼び出すことができます。 同期コードを非同期コードにするには、同期メソッドの代わりに非同期メソッドを呼び出して、コードにいくつかのキーワードを追加するだけで済みます。

ファイル アクセスの呼び出しに非同期性を適用する利点には、次のようなものがあります。

  • 非同期性により、UI アプリケーションの応答性が向上します。非同期処理を開始した UI スレッドが他の処理を実行できるためです。 UI スレッドが、時間のかかるコード、たとえば 50 ミリ秒を超えるコードを実行する必要がある場合、I/O が完了して、UI スレッドがキーボードやマウス入力などのイベントを再度処理できるようになるまで、UI が停止することがあります。
  • 非同期性を適用すると、スレッドの必要性が軽減され、ASP.NET などのサーバー ベースのアプリケーションのスケーラビリティが向上します。 アプリケーションが応答ごとに専用スレッドを使用している場合、1,000 個の要求を同時に処理するには、1,000 個のスレッドが必要です。 非同期操作では、待機中にスレッドを使用する必要はほとんどありません。 既存の I/O 完了スレッドが最後に少しだけ使用されます。
  • 現状ではファイル アクセス操作の待機時間が非常に短くても、将来に大幅に長くなる可能性があります。 たとえば、地球の裏側にあるサーバーにファイルが移動される場合があります。
  • 非同期機能の使用に伴うオーバーヘッドはわずかです。
  • 非同期タスクは簡単に並列実行できます。

適切なクラスを使用する

このトピックの簡単な例では、File.WriteAllTextAsyncFile.ReadAllTextAsync について説明します。 ファイル I/O 操作を微調整するには、FileStream クラスを使用します。これには、オペレーティング システムのレベルで非同期 I/O を発生させるオプションがあります。 このオプションを使用することにより、多くの場合、スレッド プール スレッドがブロックされるのを回避できます。 このオプションを有効にするには、コンストラクター呼び出しで useAsync=true または options=FileOptions.Asynchronous 引数を指定します。

ファイル パスを指定して StreamReaderStreamWriter を直接開いた場合、それらでこのオプションを使用することはできません。 一方、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 の評価によって Int32numRead が生成されます。 詳しくは、「非同期の戻り値の型 (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 を指定して、途中で処理をキャンセルすることができます。 詳細については、「マネージド スレッドのキャンセル」を参照してください。

関連項目