【问题标题】:How to Async Files.ReadAllLines and await for results?如何异步 Files.ReadAllLines 并等待结果?
【发布时间】:2012-10-31 21:48:03
【问题描述】:

我有以下代码,

    private void button1_Click(object sender, RoutedEventArgs e)
    {
        button1.IsEnabled = false;

        var s = File.ReadAllLines("Words.txt").ToList(); // my WPF app hangs here
        // do something with s

        button1.IsEnabled = true;
    }

Words.txt 有大量单词我读入 s 变量,我试图在 C# 5 中使用 Async CTP Library 使用 asyncawait 关键字,因此 WPF 应用程序不会挂起。到目前为止,我有以下代码,

    private async void button1_Click(object sender, RoutedEventArgs e)
    {
        button1.IsEnabled = false;

        Task<string[]> ws = Task.Factory.FromAsync<string[]>(
            // What do i have here? there are so many overloads
            ); // is this the right way to do?

        var s = await File.ReadAllLines("Words.txt").ToList();  // what more do i do here apart from having the await keyword?
        // do something with s

        button1.IsEnabled = true;
    }

目标是以异步方式而不是同步方式读取文件,以避免 WPF 应用程序冻结。

感谢任何帮助,谢谢!

【问题讨论】:

  • 如何从删除不必要的 ToList() 调用开始,这将复制字符串数组?
  • @JbEvain - 为了迂腐,ToList() 不只是复​​制数组,它创建了一个List。如果没有更多信息,您不能假设它是不必要的,因为可能“// do something with s”调用了List 方法。

标签: c# .net asynchronous async-await c#-5.0


【解决方案1】:

更新File.ReadAll[Lines|Bytes|Text]File.AppendAll[Lines|Text]File.WriteAll[Lines|Bytes|Text] 的异步版本现在是 merged into .NET Core 并随 .NET Core 2.0 一起提供。它们也包含在 .NET Standard 2.1 中。

使用Task.Run,它本质上是Task.Factory.StartNew 的包装器,用于异步包装器is a code smell

如果你不想使用阻塞函数浪费一个 CPU 线程,你应该等待一个真正的异步 IO 方法,StreamReader.ReadToEndAsync,像这样:

using (var reader = File.OpenText("Words.txt"))
{
    var fileText = await reader.ReadToEndAsync();
    // Do something with fileText...
}

这会将整个文件作为string 而不是List&lt;string&gt;。如果您需要换行,则可以在之后轻松拆分字符串,如下所示:

using (var reader = File.OpenText("Words.txt"))
{
    var fileText = await reader.ReadToEndAsync();
    return fileText.Split(new[] { Environment.NewLine }, StringSplitOptions.None);
}

编辑:这里有一些方法可以实现与File.ReadAllLines 相同的代码,但以真正异步的方式。代码基于File.ReadAllLines本身的实现:

using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading.Tasks;

public static class FileEx
{
    /// <summary>
    /// This is the same default buffer size as
    /// <see cref="StreamReader"/> and <see cref="FileStream"/>.
    /// </summary>
    private const int DefaultBufferSize = 4096;

    /// <summary>
    /// Indicates that
    /// 1. The file is to be used for asynchronous reading.
    /// 2. The file is to be accessed sequentially from beginning to end.
    /// </summary>
    private const FileOptions DefaultOptions = FileOptions.Asynchronous | FileOptions.SequentialScan;

    public static Task<string[]> ReadAllLinesAsync(string path)
    {
        return ReadAllLinesAsync(path, Encoding.UTF8);
    }

    public static async Task<string[]> ReadAllLinesAsync(string path, Encoding encoding)
    {
        var lines = new List<string>();

        // Open the FileStream with the same FileMode, FileAccess
        // and FileShare as a call to File.OpenText would've done.
        using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultBufferSize, DefaultOptions))
        using (var reader = new StreamReader(stream, encoding))
        {
            string line;
            while ((line = await reader.ReadLineAsync()) != null)
            {
                lines.Add(line);
            }
        }

        return lines.ToArray();
    }
}

【讨论】:

  • 这很重要,它使用 Windows I/O 端口在没有任何 CPU 线程的情况下等待它,而另一个答案中的 Task.Factory.StartNew/Task.Run 方法浪费了一个 CPU 线程。这个答案的方法更有效。
  • 仅供参考;我已经在github.com/dotnet/corefx/issues/11220 提出了这些 API 的异步版本。让我们看看它是怎么回事:)
  • 我会返回List,调用者可以决定是否真的需要数组,避免lines.ToArray造成的额外分配。
  • 如果效率非常低,第二个答案。解析包含数百万行的大型日志的时间比简单的 File.ReadAllLines 长 16 倍。
  • 第二个答案?你的数据在哪里?
【解决方案2】:

这是我为 NetStandart 2.0 class library 创建的辅助方法,在 NetCore 3.1NetFramework 4.7.2 项目中都使用过。

这些实现与net core 3.1 / net standard 2.1 File类方法的名称和签名完全匹配,因此您只需将它们放在任何公共类中即可。 (例如 FileHelper...):

另外,这应该是最有效的,并且类似于 .net 实现的源代码。

    private const int DefaultBufferSize = 4096;
    // File accessed asynchronous reading and sequentially from beginning to end.
    private const FileOptions DefaultOptions = FileOptions.Asynchronous | FileOptions.SequentialScan;

    public static async Task WriteAllTextAsync(string filePath, string text)
    {
        byte[] encodedText = Encoding.Unicode.GetBytes(text);

        using FileStream sourceStream = new FileStream(filePath, FileMode.Append, FileAccess.Write, FileShare.None,
            DefaultBufferSize, true);
        await sourceStream.WriteAsync(encodedText, 0, encodedText.Length);
    }

    public static async Task<IEnumerable<string>> ReadAllLinesAsync(string filePath)
    {
        var lines = new List<string>();

        using var sourceStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read,
            DefaultBufferSize, DefaultOptions);
        using var reader = new StreamReader(sourceStream, Encoding.Unicode);
        string line;
        while ((line = await reader.ReadLineAsync()) != null) lines.Add(line);

        return lines;
    }

    public static async Task<string> ReadAllTextAsync(string filePath)
    {
        using var sourceStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read,
            DefaultBufferSize, DefaultOptions);
        using var reader = new StreamReader(sourceStream, Encoding.Unicode);
        return await reader.ReadToEndAsync();
    }

编辑

显然,StreamReader“异步”方法在返回不完整的任务之前会阻塞当前线程相当长的时间。

(即使 netcore 3.1 File.ReadAllLinesAsyn,File.ReadAllTextAsync 目前似乎也不是完全异步的。您可以查看源代码,它们基于 StreamReader“异步”方法)。

所以,我正在分享一个似乎是目前最有效的实现方式。 \

这比在Task.Run(()=&gt;File.ReadAllLines(...)) 中运行同步方法等选项要好,因为用Task.Run 包装您的 sync 代码并期望这是完整的 async 是一种非常糟糕的做法 流动。 \ 实际上,它打破了真正异步dotnet结构的内部队列机制。

public static async Task<string> ReadAllTextAsync(string filePath)
    {
        using (var sourceStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read,
            DefaultBufferSize, DefaultOptions))
        {
            var sb = new StringBuilder();
            var buffer = new byte[0x1000];
            var numRead = 0;

            while ((numRead = await sourceStream.ReadAsync(buffer, 0, buffer.Length)) != 0)
                sb.Append(Encoding.Unicode.GetString(buffer, 0, numRead));
            return sb.ToString();
        }
    }

测试时间

这是我的测试及其输出,清楚地显示实际运行异步:

        var stopwatch = Stopwatch.StartNew();
        var fileTask = FileHelper.ReadAllTextAsync("48MB_file.txt");
        var duration1 = stopwatch.ElapsedMilliseconds;
        var isCompleted = fileTask.IsCompleted;

        stopwatch.Restart();
        await fileTask;
        var duration2 = stopwatch.ElapsedMilliseconds;

        Console.WriteLine($"Creation took: {duration1:#,0} ms, Task.IsCompleted: {isCompleted}");
        Console.WriteLine($"Calling await took:  {duration2:#,0} ms, Task.IsCompleted: {fileTask.IsCompleted}");

创建耗时:43 毫秒,Task.IsCompleted:False
调用 await 耗时:508 毫秒,Task.IsCompleted:True

您可以在 cmets 中找到更多信息,在这个问题中:File.ReadAllLinesAsync() blocks the UI thread

【讨论】:

  • 您是否验证过WriteAllTextAsyncReadAllLinesAsyncReadAllTextAsync方法实际上是异步的,它们不会像同步API一样阻塞调用者File.WriteAllTextFile.ReadAllLinesFile.ReadAllText?您可以通过将创建的Task 存储在一个变量中来验证它,并在等待它之前检查它的IsCompleted 属性。如果IsCompletedtrue,那么就所有意图和目的而言,调用都是100% 同步的。
  • @TheodorZoulias 我不确定这是一个正确的测试,如果他们选择异步方法可以同步完成,或者因为:他们已经知道答案(缓存结果,缓冲数据......)或者他们根本没有合适的异步下游操作来执行。需要想一个更好的方法来检查是否没有阻塞主线程。
  • 无论如何,我会根据 MSDN 示例发布另一个实现,并且按照您的建议,使用 IsCompleted 的测试确实证实了这一点,所以谢谢!
  • 你可能想看看这个问题:Why File.ReadAllLinesAsync() blocks the UI thread? 异步文件系统 API 的东西比你想象的更模糊。 Microsoft 将在下一个 .NET 版本中发布FileStreamimproved implementation,我还没有检查过。
  • 其实我自己想出来的,在net core 3.1中测试源代码时,我看到它确实在等待文件读取,即使我没有等待它,也在使用ConfigureAwait(false ) 在 reader.ReadAsync 方法上。我还在这里看到了你的评论:py4u.net/discuss/707047。但不要认为 Task.Run 是一个更好的选择,原因有几个。请在我的回答中查看我的编辑。
【解决方案3】:
private async Task<string> readFile(string sourceFilePath)
    {
        using (var fileStream = new FileStream(sourceFilePath, FileMode.Open, FileAccess.ReadWrite, FileShare.ReadWrite))
        {
            using (var streamReader = new StreamReader(fileStream))
            {
                string data = await streamReader.ReadToEndAsync().ConfigureAwait(false);
                streamReader.Close();
                fileStream.Close();
                return data;
            }

        }
    }

【讨论】:

    【解决方案4】:

    试试这个:

    private async void button1_Click(object sender, RoutedEventArgs e)
    {
        button1.IsEnabled = false;
        try
        {
            var s = await Task.Run(() => File.ReadAllLines("Words.txt").ToList());
            // do something with s
        }
        finally
        {
            button1.IsEnabled = true;
        }
    }
    

    编辑:

    您不需要 try-finally 即可。这实际上只是您需要更改的一行。解释它是如何工作的:这会产生另一个线程(实际上是从线程池中获取一个)并让该线程读取文件。当文件完成读取后,将调用 button1_Click 方法的其余部分(从 GUI 线程)并得到结果。请注意,这可能不是最有效的解决方案,但它可能是对您的代码进行的最简单的更改,不会阻塞 GUI。

    【讨论】:

    • 工作就像一个魅力!!!谢谢迈克,我也可以将Task.Factory.StartNew(() =&gt; 'Some Task') 应用于其他任务,再次感谢:)
    • 虽然这无疑是最简单的解决方案,并且对于简单的 GUI 应用程序来说很可能已经足够好了,但它并没有充分利用 async,因为它仍然会阻塞线程。
    • @svick 谢谢,我将答案编辑为Task.Run() 而不是Task.Factory.StartNew。我完全同意线程阻塞。这是否是一个问题取决于情况(如你所说)。当然,对于通过 GUI 读取一些文件,我认为开销可以忽略不计。
    • 这会解除对 UI 线程的阻塞,但会在读取期间阻塞另一个线程池线程。
    【解决方案5】:

    我还遇到了您的问题中描述的问题。我在以前的答案中更简单地解决了它:

    string[] values;
    StorageFolder folder = ApplicationData.Current.LocalFolder; // Put your location here.
    IList<string> lines = await FileIO.ReadLinesAsync(await folder.GetFileAsync("Words.txt"););
    lines.CopyTo(values, 0);
    

    【讨论】:

    • ApplicationDataFileIO 的类从何而来?它们似乎不是 .Net 框架的一部分。 ApplicationData 似乎来自 UWP Framework。这意味着您不能在“普通”.net 应用程序中使用ApplicationDataFileIO 存在于 VisualBasic assembly 中,但据我所知没有异步方法,那么你从哪里得到它?
    • @AndyJ,是的,我的解决方案适用于 UWP 应用程序。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2021-10-22
    • 2016-12-05
    • 2018-08-28
    • 1970-01-01
    • 2015-10-06
    • 2020-06-03
    • 1970-01-01
    相关资源
    最近更新 更多