80

我有以下代码,

    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 中使用asyncawait关键字,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 应用程序冻结。

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

4

5 回答 5

152

更新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...
}

这会将整个文件作为 astring而不是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();
    }
}
于 2012-10-31T21:54:42.567 回答
0

以下是我为 a 创建的辅助方法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.1File.ReadAllLinesAsyn,File.ReadAllTextAsync似乎也不是完全异步的。您可以查看源代码,它们基于 StreamReader“异步”方法)。

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

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

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
调用等待耗时: 508毫秒,Task.IsCompleted:True

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

于 2021-09-13T11:07:06.317 回答
-1
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;
            }

        }
    }
于 2018-10-16T07:19:09.530 回答
-3

试试这个:

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。

于 2012-10-31T21:54:49.317 回答
-4

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

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);
于 2017-01-07T11:46:30.920 回答