非同步檔案存取 (C#)

您可以使用非同步功能來存取檔案。 使用非同步功能,您就可以呼叫非同步方法,而不需要使用回呼或將您的程式碼分散到多種方法或 Lambda 運算式上。 若要讓同步程式碼變成非同步,只要呼叫非同步方法 (而不是同步方法),然後將幾個關鍵字新增至程式碼即可。

您可能會基於下列原因將非同步功能新增至檔案存取呼叫:

  • 非同步功能可讓 UI 應用程式回應速度更快,因為啟動作業的 UI 執行緒可以執行其他工作。 如果 UI 執行緒必須執行相當耗時的程式碼 (例如超過 50 毫秒),UI 可能會凍結到 I/O 完成,之後 UI 執行緒就可以再次處理鍵盤和滑鼠輸入以及其他事件。
  • 非同步功能藉由降低對執行緒的需求,來改善 ASP.NET 及其他伺服器架構應用程式的延展性。 如果應用程式針對每個回應使用專用執行緒,並同時處理一千個要求,則需要一千個執行緒。 非同步作業在等候期間通常不需要使用執行緒。 它們可能會在結束時短暫使用現有的 I/O 完成執行緒。
  • 檔案存取作業的延遲在目前情況下可能很低,但未來可能會大幅增加。 例如,檔案可能會移至世界各地的伺服器。
  • 使用非同步功能所增加的額外負荷很小。
  • 您可以輕鬆地同時執行多個非同步工作。

使用適當類別

本主題中的簡單範例示範 File.WriteAllTextAsyncFile.ReadAllTextAsync。 如果要精細控制檔案 I/O 作業,請使用 FileStream 類別,其中會有導致作業系統層級發生非同步 I/O 的選項。 在許多情況下,您可以使用此選項來避免封鎖 ThreadPool 執行緒。 若要啟用此選項,請在建構函式呼叫中指定 useAsync=trueoptions=FileOptions.Asynchronous 引數。

如果您藉由指定檔案路徑來直接開啟 StreamReaderStreamWriter,就無法搭配使用這個選項。 不過,如果您提供它們已開啟 FileStream 類別的 Stream,則可以使用此選項。 即使已封鎖 ThreadPool 執行緒,非同步呼叫在 UI 應用程式中還是更快,因為等候期間並不會封鎖 UI 執行緒。

寫入文字

下列範例會將文字寫入檔案。 在每個 await 陳述式中,此方法會立即結束。 當檔案 I/O 完成時,此方法會在 await 陳述式後面的陳述式繼續進行。 使用 await 陳述式的方法定義中會有 async 修飾詞。

簡單範例

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);,這是下列兩個陳述式的縮減:

Task theTask = sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
await theTask;

第一個陳述式會傳回一個工作並開始處理檔案。 第二個陳述式具有 await,會立即結束方法並傳回其他工作。 稍後完成處理檔案之後,則會回到 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 陳述式中建立每個 FileStreamFileStream 可能會在工作完成前就被處置。

任何效能提升幾乎完全來自平行處理,而不是非同步處理。 非同步的優點在於不會佔用多個執行緒,而且不會佔用使用者介面執行緒。

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();
        }
    }
}

使用 WriteAsyncReadAsync 方法時,您可以指定 CancellationToken,這可用來在中途取消作業。 如需詳細資訊,請參閱受控執行緒中的取消作業

另請參閱