如何异步Files.ReadAllLines并等待结果?


73

我有以下代码,

    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中的asyncandawait关键字,Async CTP Library因此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应用程序。

任何帮助表示赞赏,谢谢!


1
从删除不必要的对ToList()的调用开始,该调用将复制字符串数组呢?
伊布

3
@JbEvain-要学究,ToList()不只是复制数组,它还会创建一个List。没有更多的信息,您将无法假设它是不必要的,因为“ // do something with s”可能会调用List方法。
迈克

Answers:


142

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

对于异步包装Task.Run器来说Task.Factory.StartNew,使用本质上是包装器的代码是一种味道

如果您不想通过使用阻塞函数来浪费CPU线程,则应等待真正的异步IO方法StreamReader.ReadToEndAsync,如下所示:

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

这将得到整个文件,string而不是一个List<string>。如果您需要换行,则可以在以后轻松拆分字符串,如下所示:

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

11
这很重要,它使用Windows I / O端口在没有任何CPU线程的情况下等待此事件,而另一个答案中的Task.Factory.StartNew / Task.Run方法浪费了CPU线程。这个答案的方法更有效。
克里斯·莫斯基尼

1
仅供参考;我已经在github.com/dotnet/corefx/issues/11220上提出了这些API的异步版本。让我们看看它是怎么回事:)
khellang '16

我将返回List <string>,调用方可以确定它是否确实需要数组,并避免由引起的额外分配lines.ToArray
史蒂夫斯16/09/25

如果效率很低,第二个答案。与几行简单的File.ReadAllLines相比,解析成百万行的大型日志的时间要长16倍。
ghord '02

第二个答案?你的资料呢
khellang'2

0

使用Stream.ReadAsync异步读取文件,

private async void Button_Click(object sender, RoutedEventArgs e)
{
    string filename = @"c:\Temp\userinputlog.txt";
    byte[] result;

    using (FileStream SourceStream = File.Open(filename, FileMode.Open))
    {
        result = new byte[SourceStream.Length];
        await SourceStream.ReadAsync(result, 0, (int)SourceStream.Length);
    }

    UserInput.Text = System.Text.Encoding.ASCII.GetString(result);
}

读取MSDN Stream.ReadAsync


0

如果要异步读取文件中的所有行,则可以使用来使用该async功能访问文件FileStream

private static async Task<string[]> ReadAllLinesAsync(string filePath)
    {
        using (FileStream sourceStream = new FileStream(filePath,
            FileMode.Open, FileAccess.Read, FileShare.Read,
            bufferSize: 4096, useAsync: true))
        {
            StringBuilder 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().Split(new[] { Environment.NewLine },StringSplitOptions.None);
        }
    }

您可以async通过指定async事件处理函数来在事件处理函数中使用该方法。

这是您可以使用的方式,这不会让您的GUI线程冻结。

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

    var s = await ReadAllLinesAsync("Words.txt").ToList();
    // do something with s

    button1.IsEnabled = true;
}

有关更多详细信息,请参阅MS Docs。


-3

我还遇到了您的问题中所述的问题。在以前的答案中,我已经解决了这一问题:

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

2
哪里做类ApplicationDataFileIO从何而来?它们似乎不是.Net Framework的一部分。ApplicationData似乎来自UWP Framework。这意味着您不能ApplicationData在“普通” .net应用程序中使用。FileIOVisualBasic程序集中存在,但据我所知没有异步方法,那么您从哪里得到它?

@AndyJ,是的,我的解决方案是针对UWP应用程序的。
安东·巴库列夫

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

编辑:

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


像魅力一样工作!!!Task.Factory.StartNew(() => 'Some Task')

12
尽管这当然是最简单的解决方案,并且对于一个简单的GUI应用程序来说,它很可能已经足够好了,但async由于它仍然会阻塞线程,因此无法充分发挥其潜力。
svick

@svick谢谢,我已经编辑了答案,Task.Run()而不是Task.Factory.StartNew。我完全同意线程阻塞。这是否是一个问题取决于情况(如您所说)。当然,对于通过GUI读取一些文件,我认为开销可以忽略不计。
Mike

1
这将取消阻止UI线程,但在读取期间将阻止另一个线程池线程。
Mark
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.