异步文件访问 (C#)

通过使用异步功能访问文件,可以调用异步方法,而无需使用回调或跨多个方法或 lambda 表达式拆分代码。 若要使同步代码异步,请调用异步方法而不是同步方法,并向代码添加几个关键字。

请考虑出于以下原因将异步添加到文件访问调用:

  • Asynchrony 使 UI 应用程序更具响应性,因为启动操作的 UI 线程可以执行其他工作。 如果 UI 线程必须执行长时间(例如超过 50 毫秒)的代码,UI 可能会冻结,直到 I/O 完成,UI 线程可以再次处理键盘和鼠标输入和其他事件。
  • Asynchrony 通过减少线程需求来提高 ASP.NET 和其他基于服务器的应用程序的可伸缩性。 如果应用程序每个响应使用专用线程,并且同时处理一千个请求,则需要一千个线程。 异步操作通常不需要在等待期间使用线程。 异步操作仅需在结束时短暂使用现有 I/O 完成线程。
  • 在当前条件下,文件访问操作的延迟可能非常低,但将来的延迟可能会大大增加。 例如,文件可能移动到世界各地的服务器。
  • 使用异步功能的附加开销很小。
  • 多个异步 I/O 操作可以在不阻止调用线程的情况下运行。

使用适当的类

本主题中的简单示例演示 File.WriteAllTextAsyncFile.ReadAllTextAsync。 若要精细控制文件 I/O 操作,请使用 FileStream 类,该类具有导致异步 I/O 在操作系统级别发生的选项。 使用此选项可以避免在许多情况下阻塞线程池线程。 若要启用此选项,请在构造函数调用中指定 useAsync=trueoptions=FileOptions.Asynchronous 参数。

如果通过指定文件路径直接打开 StreamReaderStreamWriter,则无法将此选项与这二者配合使用。 但是,如果为二者提供已由 FileStream 类打开的 Stream,则可以使用此选项。 即使线程池线程被阻塞,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>,因此在操作完成后 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();
        }
    }
}

使用 WriteAsyncReadAsync 方法时,可以指定一个 CancellationToken 在过程中取消操作。 有关详细信息,请参阅托管线程中的取消

另见