非同步檔案存取(C#)

透過非同步功能存取檔案,你可以呼叫非同步方法,而不必使用回調,也不會將程式碼拆分成多個方法或 lambda 表達式。 要讓同步程式碼變成非同步,請呼叫非同步方法,並在程式碼中加入幾個關鍵字。

考慮為檔案存取呼叫加入非同步功能,原因如下:

  • 非同步讓 UI 應用程式更靈敏,因為啟動操作的 UI 執行緒可以執行其他工作。 若 UI 執行緒必須執行耗時較長的程式碼(例如超過 50 毫秒),UI 可能會凍結,直到 I/O 完成後,UI 執行緒才能再次處理鍵盤滑鼠輸入及其他事件。
  • 非同步透過減少執行緒需求,提升 ASP.NET 及其他伺服器端應用程式的可擴展性。 如果應用程式每個回應使用專用執行緒,且同時處理一千個請求,則需要一千個執行緒。 非同步操作通常不需要在等待期間使用執行緒。 他們在最後短暫使用現有的 I/O 完成執行緒。
  • 在目前條件下,檔案存取操作的延遲可能非常低,但未來延遲可能會大幅增加。 例如,一個檔案可能會被移到全球另一端的伺服器。
  • 使用非同步功能所帶來的額外負擔很小。
  • 多個非同步 I/O 操作可以在不阻塞呼叫執行緒的情況下執行。

使用適當的類別

本主題中的簡單例子展示了 File.WriteAllTextAsyncFile.ReadAllTextAsync。 若要細緻控制檔案 I/O 操作,請使用 類別 FileStream ,該類別有一個選項能在作業系統層級發生非同步 I/O。 使用此選項,您可以在許多情況下避免阻塞執行緒池執行緒。 要啟用此選項,請在建構子呼叫中指定 useAsync=true or options=FileOptions.Asynchronous 參數。

如果你直接透過指定檔案路徑來開啟,就無法使用這個選項StreamReaderStreamWriter。 不過,如果你提供StreamFileStream課程開設的資訊,就可以使用這個選項。 即使執行緒池執行緒被阻塞,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);,這是以下兩個陳述的縮寫:

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>,因此等候的評估在操作完成後會產生一個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();
}

多重非同步輸入輸出操作

以下範例可啟動多個非同步寫入操作。 執行時會排隊這些操作,底層實作可能會根據平台與設定使用作業系統的非同步輸入輸出或執行緒池執行緒,因此實際的並行性取決於作業系統與硬體。

簡單範例

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 待處理時阻塞呼叫執行緒。 在許多情況下,吞吐量的提升取決於作業系統、硬體,以及某些平台上的 .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();
        }
    }
}

使用 WriteAsyncReadAsync 方法時,你可以指定 CancellationToken 以便於中途取消操作。 如需詳細資訊,請參閱 受管控執行緒中的取消

另請參閱