Laden...

Erster Versuch mit async/await

Erstellt von OlafSt vor 8 Jahren Letzter Beitrag vor 8 Jahren 3.401 Views
O
OlafSt Themenstarter:in
79 Beiträge seit 2011
vor 8 Jahren
Erster Versuch mit async/await

Hallo Freunde,

ich habe mich mal an ein für mich nützliches Utility gemacht. Im wesentlichen ist das nur ein Dateiscanner, der aber die gefundenen Dateien später im gesamten Haus-LAN verfügbar machen soll.

Kernstück des ganzen ist natürlich die Dateisuche und mich nervte die Tatsache, das die GUI dabei einfriert. Schreit ja nach async/await 😁

Zum testen also einfach ein WinForm aufgemacht, ein Menü drauf, eine Listbox und zwei Labels.

Der Handler für den Menüpunkt wirft die ganze Arie an und erwartet eine Liste mit Dateinamen zurück. Diese Liste wird in die Listbox gefüllt und noch ein bissel Prosa ausgegeben:


        private async void filmeToolStripMenuItem_Click(object sender, EventArgs e)
        {
            FileScanClass fsc = new FileScanClass();
            fsc.OnScanDirectory += OnScanDirectory;


            List<string> li=await fsc.ScanFilesRecursiveAsync("C:\\", new string[] {"*.ts"});
            for(int i=0; i<li.Count;i++)
               listBox1.Items.Add(li[i]);
            label1.Text = "Durchsuchtes Verzeichnis: fertig";
            MessageBox.Show(string.Format("Scan complete. Directories scanned: {0}. Files found: {1}", fsc.DirsScannedCount, li.Count));
        }

Ich habe das ganze Filescanning in eine eigene Klasse verpackt, die aus Jux und Dollerei für jedes durchsuchte Verzeichnis einen Event auslöst. So kann die GUI dem Benutzer anzeigen, das was passiert - etwas, das seit Windows 8 völlig aus der Mode kommt.

Durch Async/Await scheint das ganze in einem eigenen Thread zu laufen, weshalb hier kräftig Invoked werden muß:


        private void OnScanDirectory(object sender, ScanDirectoryEventArgs e)
        {
            if (label1.InvokeRequired)
            {
                label1.Invoke(new MethodInvoker(delegate
                {
                    label1.Text = "Durchsuchtes Verzeichnis: " + e.DirectoryName;
                    label2.Text = "Durchsuchte Verzeichnisse: " + e.ScanCounter.ToString();
                    label1.Update();
                    label2.Update();
                }));
            }
            else
            {
                label1.Text = "Durchsuchtes Verzeichnis: " + e.DirectoryName;
                label2.Text = "Durchsuchte Verzeichnisse: " + e.ScanCounter.ToString();
                label1.Update();
                label2.Update();
            }
        }

Fehlt uns nur noch die eigentliche Dateisuche selbst, respektive die Klasse, die ich dazu gebastelt habe (///-Tags entfernt, der Post ist schon lang genug):


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

namespace MScanner
{
    public class ScanDirectoryEventArgs : EventArgs
    {
        private string DirName;
        private long ScanCount;

        public ScanDirectoryEventArgs(string Dir, long ScanCounter)
        { 
            DirName = Dir;
            ScanCount=ScanCounter;
        }
        public string DirectoryName
        {
            get
            {
                return DirName;
            }
        }
        public long ScanCounter
        { 
            get 
            { 
                return ScanCount; 
            } 
        }
    }

    class FileScanClass
    {
        public delegate void ScanDirectoryEventHandler(object source, ScanDirectoryEventArgs e);
        public event ScanDirectoryEventHandler OnScanDirectory;
        private long _DirCount;

        public long DirsScannedCount { get {return _DirCount;} }

        public FileScanClass()
        {
        }

        public async Task<List<string>> ScanFilesAsync(string SourceDir, string Extension)
        {
            return await ScanFilesAsync(SourceDir, new string[] { Extension });
        }

        public async Task<List<string>> ScanFilesAsync(string SourceDir, string[] Extensions)
        {
            return await Task<List<string>>.Run(() =>
                {
                    List<string> li = new List<string>();
                    DirectoryInfo di = new DirectoryInfo(SourceDir);
                    if (OnScanDirectory != null)
                        OnScanDirectory(this, new ScanDirectoryEventArgs(SourceDir, _DirCount));
                    _DirCount++;  //Kein wirkliches Multithreading, sollte also safe sein
                    foreach (string s in Extensions)
                    {
                        FileInfo[] fi = di.GetFiles(s);
                        foreach (FileInfo f in fi)
                        {
                            li.Add(f.FullName);
                        }
                    }
                    return li;
                });
        }

        public async Task<List<string>> ScanFilesRecursiveAsync(string SourceDir, string[] Extensions)
        {
            return await Task<List<string>>.Run(() =>
            {
                DirectoryInfo[] Dirs;

                List<string> li = new List<string>();
                DirectoryInfo di = new DirectoryInfo(SourceDir);
                if (OnScanDirectory != null)
                    OnScanDirectory(this, new ScanDirectoryEventArgs(SourceDir, _DirCount));
                Interlocked.Increment(ref _DirCount); //Durch Multithreading muß das abgesichert sein
                try
                {
                    Dirs = di.GetDirectories();
                }
                catch (UnauthorizedAccessException)
                {
                    //Kein Zugriff auf dieses Verzeichnis, also leere Liste
                    //und weg hier
                    return li;
                }
                foreach(DirectoryInfo DirInfo in Dirs)
                {
                    var Res=ScanFilesRecursiveAsync(DirInfo.FullName, Extensions);
                    li.AddRange(Res.Result);
                }
                foreach (string s in Extensions)
                {
                    FileInfo[] fi = di.GetFiles(s);
                    foreach (FileInfo f in fi)
                    {
                        li.Add(f.FullName);
                    }
                }
                return li;
            });
        }
    }
}

Es gibt dort zwei Methoden, die die Arbeit verrichten. ReadFilesAsync liest nur ein einzelnes Directotry aus; ReadFilesRecursiveAsync macht das auch, wandert aber auch durch eventuell vorhandene Unterverzeichnisse (underen Unterverzeichnisse etc) hindurch. Von Interesse ist auch nur die rekursive Methode, diese wird primär zum Einsatz kommen.

Meine Frage zu diesem Stück Code:

Es entstehen gewaltige Mengen an Threads, wenn diese Routine z.B. auf C:\ losgelassen wird (bis zu 40 zeigt der Taskmanager an). Da mir die Funktion von Async/Await noch immer ein wenig schleierhaft ist: Laufen diese wirklich alle zeitgleich oder wartet der Task, der gerade C:\Windows durchsucht, auf den Task, der C:\Windows\System32 durchsucht - der auf den Task wartet, der C:\Windows\System32\Microsoft durchsucht ?

Danke fürs drüberschauen.

W
955 Beiträge seit 2010
vor 8 Jahren

Hi,

es ist sicherlich ungünstig wenn mehrere Tasks gleichzeitig die Festplatte durchsuchen da hier der Schreib/Lesekopf auf der Platte nur hin- und herspringt und dadurch noch viel mehr Zeit verbraucht wird.
* verwende einen Task der die Arbeit macht
* verwende CancellationToken um die Arbeit abbrechen zu können
* verwende IProgress<T> um den Fortschritt anzuzeigen
* Abt hat glaube ich eine Lib dafür entwickelt, vllt kannst Du diese verwenden

O
OlafSt Themenstarter:in
79 Beiträge seit 2011
vor 8 Jahren

Darum auch meine Frage: Laufen diese Tasks wirklich alle nebeneinander her ? Wenn ja, welchen Sinn hat dann "await" ? So wie ich das verstanden habe, wartet "await" quasi doch darauf, das der Task durchgelaufen ist...

Ansonsten ist es mir schon klar, das man den Schreib-Lese-Kopf nicht überfordern sollte 😉

W
955 Beiträge seit 2010
vor 8 Jahren

Wenn ja, welchen Sinn hat dann "await" ? So wie ich das verstanden habe, wartet "await" quasi doch darauf, das der Task durchgelaufen ist... Du wartest doch gar nicht:


var Res=ScanFilesRecursiveAsync(DirInfo.FullName, Extensions);

=>


return await Task<List<string>>.Run(async () =>
...
var Res=await ScanFilesRecursiveAsync(DirInfo.FullName, Extensions);

16.807 Beiträge seit 2008
vor 8 Jahren

Das Stichwort hier wäre TPL Pipelining; umzusetzen mit der BlockingCollection.
Du kannst Dir zu einfacheren Handhabung mal mein QuickIO.NET Projekt anschauen. Async/Await wird hier unterstützt; musst es nur noch korrekt einbinden.

Mehrere Tasks zum durchsuchen sollte nur auf Netzwerklaufwerken angewendet werden; niemals gegenüber einer einzelnen HDD.

O
OlafSt Themenstarter:in
79 Beiträge seit 2011
vor 8 Jahren

Du wartest doch gar nicht:

  
var Res=ScanFilesRecursiveAsync(DirInfo.FullName, Extensions);  
  

=>

  
return await Task<List<string>>.Run(async () =>  
...  
var Res=await ScanFilesRecursiveAsync(DirInfo.FullName, Extensions);  
  

8o ⚠

Ich habe mich immer gefragt, warum ich nicht "var Res=await Scan... " aufrufen konnte - der Compiler hat sofort losgemosert. Jetzt ist mir das klar: Der Delegat muß auch einen async-Modifizierer haben. Einmal async, immer und überall async.

Mit dieser Änderung entstehen nun erheblich weniger Threads (17, ab und zu ein 18.) - wie ich es eigentlich schon am Anfang erwartet hatte. Dem Gerappel auf der Platte nach zu urteilen warten die nun auch fleißig aufeinander. Nichts anderes wollte ich erreichen: Es sollte immer nur ein Task zur Zeit aktiv mit dem Laufwerk arbeiten, alle anderen sollten abwarten (bin schon vor 2 Dekaden, als Multithreading aufkam, über dieses problem gestolpert). Zugleich will ich aber meine GUI aktiv behalten.

Cleverer waäre es natürlich gewesen, das gleich in einen einzelnen Task oder Thread zu stecken, aber dann hätte ich halt nicht kapiert, wie async/await funktioniert und wie es korrekt benutzt wird.

Ist euch sonst noch eine Häßlichkeit aufgefallen, die man als fähiger C#-Programmierer nicht machen sollte ?

16.807 Beiträge seit 2008
vor 8 Jahren

async/await arbeitet mit Tasks, nicht mit Threads.
Die Herleitung ist also nicht korrekt. Die Zahl ergibt sich vom Scheduler, den Du im Standard so nicht beeinflussen kannst.