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

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

次の理由から、ファイル アクセス呼び出しに非同期を追加することを検討してください。

  • 非同期では、操作を起動する UI スレッドが他の作業を実行できるため、UI アプリケーションの応答性が向上します。 UI スレッドで時間がかかるコードを実行する必要がある場合 (たとえば、50 ミリ秒を超える場合)、I/O が完了し、UI スレッドがキーボードやマウス入力などのイベントを再び処理できるようになるまで UI がフリーズする可能性があります。
  • 非同期では、スレッドの必要性を減らすことで、ASP.NET やその他のサーバー ベースのアプリケーションのスケーラビリティが向上します。 アプリケーションが応答ごとに専用スレッドを使用し、1,000 個の要求が同時に処理されている場合は、1000 個のスレッドが必要です。 非同期操作では、多くの場合、待機中にスレッドを使用する必要はありません。 最後に既存の I/O 完了スレッドを短時間使用します。
  • ファイル アクセス操作の待機時間は、現在の条件下では非常に短くなる可能性がありますが、待ち時間は今後大幅に増加する可能性があります。 たとえば、世界中のサーバーにファイルが移動される場合があります。
  • 非同期機能を使用する場合の追加オーバーヘッドは小さくなります。
  • 呼び出し元のスレッドをブロックすることなく、複数の非同期 I/O 操作を実行できます。

適切なクラスを使用する

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

ファイル パスを指定して直接開く場合、 StreamReaderStreamWriter でこのオプションを使用することはできません。 ただし、Stream クラスが開いたFileStreamを指定する場合は、このオプションを使用できます。 UI スレッドが待機中にブロックされないため、スレッド プール スレッドがブロックされている場合でも、UI アプリでは非同期呼び出しが高速になります。

テキストを書き込む

次の例では、ファイルにテキストを書き込みます。 各 await ステートメントで、メソッドはすぐに終了します。 ファイル I/O が完了すると、await ステートメントの後のステートメントでメソッドが再開されます。 非同期修飾子は、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 操作

次の例では、複数の非同期書き込み操作を開始します。 ランタイムはこれらの操作をキューに入れます。基になる実装では、プラットフォームと構成に応じてオペレーティング システム (OS) の非同期 I/O またはスレッド プール スレッドが使用される可能性があるため、実際のコンカレンシーは OS とハードウェアによって異なります。

単純な例

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); ステートメントはメソッドを終了し、すべてのタスクのファイル処理が完了するとメソッド内で再開します。

この例では、タスクの完了後、FileStream ブロック内のすべてのfinally インスタンスを閉じます。 各 FileStream が代わりに using ステートメントで作成された場合、タスクが完了する前に FileStream が破棄される可能性があります。

非同期アプローチでは、I/O が保留中の間に呼び出し元のスレッドがブロックされるのを回避できます。 多くの場合、スループットの向上は、OS、ハードウェア、および一部のプラットフォームでは、スレッド プールの制限やスケジュール設定などの .NET ランタイム動作によって異なります。

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を指定して、ストリームの途中で操作を取り消すことができます。 詳細については、「 マネージド スレッドでの取り消し」を参照してください。

こちらも参照ください