Wie kann ich Files.ReadAllLines asynchronisieren und auf Ergebnisse warten?

73

Ich habe den folgenden Code:

    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.txteine Tonne von Worten hat , die ich in den s - Variable lesen, versuche ich zu nutzen zu machen asyncund awaitSchlüsselwörter in C # 5 verwendet , Async CTP Libraryso dass die WPF - Anwendung nicht hängen. Bisher habe ich folgenden Code:

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

Ziel ist es, die Datei nicht synchron, sondern asynchron zu lesen, um ein Einfrieren der WPF-App zu vermeiden.

Jede Hilfe wird geschätzt, danke!

Khellang
quelle
1
Wie wäre es, wenn Sie zunächst den unnötigen Aufruf von ToList () entfernen, der eine Kopie des String-Arrays erstellt?
Jb Evain
3
@JbEvain - Um pedantisch zu sein, kopiert man ToList()nicht nur das Array, sondern erstellt ein List. Ohne weitere Informationen können Sie nicht davon ausgehen, dass dies unnötig ist, da möglicherweise Methoden // do something with saufgerufen Listwerden.
Mike

Antworten:

142

UPDATE : Async - Versionen File.ReadAll[Lines|Bytes|Text], File.AppendAll[Lines|Text]und File.WriteAll[Lines|Bytes|Text]wurden nun in .NET Kern verschmolzen und versendet mit .NET Core 2.0. Sie sind auch in .NET Standard 2.1 enthalten.

Die Verwendung von asynchronen Wrappern, für Task.Rundie es sich im Wesentlichen um einen Wrapper handelt Task.Factory.StartNew, ist ein Codegeruch .

Wenn Sie keinen CPU-Thread mithilfe einer Blockierungsfunktion verschwenden möchten, sollten Sie auf eine wirklich asynchrone E / A-Methode StreamReader.ReadToEndAsyncwie folgt warten :

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

Dadurch wird die gesamte Datei als stringstatt als erhalten List<string>. Wenn Sie stattdessen Zeilen benötigen, können Sie die Zeichenfolge anschließend wie folgt aufteilen:

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

BEARBEITEN : Hier sind einige Methoden, um den gleichen Code wie File.ReadAllLinesauf wirklich asynchrone Weise zu erzielen . Der Code basiert auf der Implementierung von sich File.ReadAllLinesselbst:

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();
    }
}
Khellang
quelle
11
Dies verwendet vor allem Windows-E / A-Ports, um dies ohne CPU-Threads abzuwarten, während der Task.Factory.StartNew / Task.Run-Ansatz in einer anderen Antwort einen CPU-Thread verschwendet. Der Ansatz dieser Antwort ist effizienter.
Chris Moschini
1
Zu Ihrer Information; Ich habe asynchrone Versionen dieser APIs unter github.com/dotnet/corefx/issues/11220 vorgeschlagen . Mal sehen, wie es geht :)
Khellang
Ich würde List <string> zurückgeben, der Aufrufer kann entscheiden, ob er wirklich ein Array benötigt, und die zusätzliche Zuordnung vermeiden, die durch verursacht wird lines.ToArray.
Steves
Zweite Antwort, wenn sehr ineffizient. Analysieren großer Protokolle mit Millionen von Zeilen, die ich bis zu 16x länger gesehen habe als einfache File.ReadAllLines.
Ghord
Zweite Antwort? Wo sind deine Daten?
Khellang
0

Verwenden Sie Stream.ReadAsync zum asynchronen Lesen von Dateien.

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

Lesen Sie MSDN Stream.ReadAsync

Mak Ahmed
quelle
0

Wenn Sie alle Zeilen asynchron aus der Datei lesen möchten, können Sie die asyncFunktion verwenden, um mit using auf die Datei zuzugreifen 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);
        }
    }

Sie können die asyncMethode im Ereignishandler verwenden, indem Sie asyncIhre Ereignishandlerfunktion angeben.

Hier erfahren Sie, wie Sie dies verwenden können, damit Ihr GUI-Thread nicht einfriert.

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

Weitere Informationen finden Sie in den MS-Dokumenten

habib
quelle
-3

Ich bin auch auf ein in Ihrer Frage beschriebenes Problem gestoßen. Ich habe es nur einfacher gelöst als in früheren Antworten:

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);
Anton Bakulev
quelle
2
Woher kommen die Klassen ApplicationDataund woher FileIO? Sie scheinen nicht Teil des .Net Frameworks zu sein. ApplicationDatascheint aus dem UWP Framework zu stammen . Das heißt, Sie können nicht ApplicationDatain einer "normalen" .net-Anwendung verwenden. FileIOexistiert in der VisualBasic-Assembly , hat aber meines Erachtens keine asynchronen Methoden. Woher beziehen Sie sie?
@AndyJ, ja, meine Lösung ist für UWP-Anwendungen.
Anton Bakulev
-4

Versuche dies:

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

Bearbeiten:

Sie brauchen den Versuch nicht, damit dies funktioniert. Es ist wirklich nur die eine Zeile, die Sie ändern müssen. So erklären Sie, wie es funktioniert: Dadurch wird ein anderer Thread erzeugt (tatsächlich wird einer aus dem Thread-Pool abgerufen) und dieser Thread wird zum Lesen der Datei aufgefordert. Wenn die Datei mit dem Lesen fertig ist, wird der Rest der button1_Click-Methode (aus dem GUI-Thread) mit dem Ergebnis aufgerufen. Beachten Sie, dass dies wahrscheinlich nicht die effizienteste Lösung ist, aber wahrscheinlich die einfachste Änderung an Ihrem Code, die die GUI nicht blockiert.

Mike
quelle
Lief wie am Schnürchen!!! Danke Mike, ich konnte Task.Factory.StartNew(() => 'Some Task')
12
Dies ist sicherlich die einfachste Lösung und wird höchstwahrscheinlich für eine einfache GUI-Anwendung gut genug sein, aber es wird nicht asyncdas volle Potenzial ausgeschöpft, da es immer noch einen Thread blockiert.
Svick
@svick Dank habe ich meine Antwort bearbeitet zu sagen , Task.Run()statt Task.Factory.StartNew. Ich stimme der Thread-Blockierung voll und ganz zu. Ob das ein Problem ist oder nicht, hängt von der Situation ab (wie Sie sagen). Für das Lesen einiger Dateien über die GUI halte ich den Overhead für vernachlässigbar.
Mike
1
Dadurch wird der UI-Thread entsperrt, aber ein anderer Thread-Pool-Thread für die Dauer des Lesevorgangs blockiert.
Mark