HowTo: Multithreading in .NET – Asynchrone Programmierung (Eventbasiert)

image

Hoch performante Systeme oder Desktopprogramme kommen ohne asynchrone Programmierung nicht weit. Wenn bei einer Windows Anwendung die Anwendung während einer Aktion "steht" und die Anwendung nicht mehr reagiert, dann wird diese Aktion synchron ausgeführt. In diesem HowTo zeige ich einen Einstieg, wie man seine Backend-Logik mit asynchronen Methoden ausstatten kann.

Synchrone Programmierung
Sehr einfach gesagt, ist synchrone Programmierung dann, wenn immer nur ein Prozess zur gleichen Zeit aktiv sein kann. Wir stellen uns eine Windows Anwendung mit einem Button vor, welche zu einer Datenbank oder eines Webservices Verbindung aufbaut um Daten zu holen und diese sollen hinterher dargestellt werden. Sobald man eine Anwendung startet, wird ein Thread gestartet (und noch einiges anderes, aber das tut jetzt nix zur Sache) wo die Anwendung drin "lebt". Als Prozesskette sähe dies so aus:

image

Sobald man den Button drückt, wird in dem Thread der Database/Webservice Call durchgeführt. Wenn der Nutzer nun noch einmal klickt, dann wird er sowas sehen:

image 

Beachte das "Keine Rückmeldung".

Würden wir jetzt verschiedene andere Systeme um Daten bitten, würde die Anwendung ewig für den Nutzer nicht "klickbar" sein.

Asynchrone Programmierung
Nett wäre ja, wenn man das darstellen der GUI und "Worker" Thread getrennt wären:

image

Damit bleibt die Anwendung trotzdem noch "klickbar" und andere Aktionen können gestartet werden.

Im .NET Umfeld gibt es drei Varianten, wie man dies realisieren kann:
- IAsyncResult Pattern
- Delegate Pattern
- Event-based Pattern

Ein guter Einstieg findet sich auch in der MSDN (und hier).
In meinem Beispiel nutze ich das Event-based Pattern, weil mir dies am "einfachsten" vorkam.

Einsatzszenarien
In Desktopanwendungen ist es natürlich meist ein muss, aber auch bei Webanwendungen ist dies interessant, damit die Anwendung besser skaliert. Ein Beispiel ist z.B. bei ASP.NET MVC die Asynchronen Controller.

"Probleme"
Neben der gesteigerten Komplexität muss man sich natürlich immer überlegen ob der Einsatz passt oder nicht. Eine asynchrone Aktion kann theoretisch Studenlang laufen oder einfach abbrechen, weil z.B. am anderen Ende die Datenbank gerade down ist. Wer mit WindowsForms oder WPF arbeitet, findet auch noch ein anderes Problem: Es kann immer nur ein Thread die GUI verändern, im WPF Umfeld gibt es da z.B. den Dispatcher. Ich zeige in diesem HowTo nur wie man im Backend eine asynchrone Operation bereitstellt. Wie man das zu einer GUI weitergibt ist ein anderes Thema, daher zeige ich dies nur in einem Konsolenprogramm.

Zum Beispiel
Wir haben ein Kundendatenbank. Ein Kunde wird repräsentiert durch die Customer Klasse:

    public class Customer
    {
        public string Name { get; set; }
        public string Address { get; set; }
        public Guid Id { get; set; }
    }

Unsere Kundendatenbank wollen wir mit einem CustomerRepository ansprechen:

    public class CustomerRepository
    {
        public List<Customer> GetCustomers()
        {
            Thread.Sleep(10000);
            List<Customer> resultList = new List<Customer>();
            resultList.Add(new Customer() { Address = "New York", Id = Guid.NewGuid(), Name = "Bank ABC" });
            resultList.Add(new Customer() { Address = "Berlin", Id = Guid.NewGuid(), Name = "Manufactor XYZ" });
            resultList.Add(new Customer() { Address = "Paris", Id = Guid.NewGuid(), Name = "Test 123" });
            resultList.Add(new Customer() { Address = "Tokyo", Id = Guid.NewGuid(), Name = "Bank DDD" });
            resultList.Add(new Customer() { Address = "London", Id = Guid.NewGuid(), Name = "Bank HHH" });
            return resultList;
        }

        public void GetCustomersAsync()
        {
            ThreadPool.QueueUserWorkItem(y =>
            {
                List<Customer> result = this.GetCustomers();
                this.OnGetCustomersAsyncCompleted(result);
            });
        }

        private void OnGetCustomersAsyncCompleted(List<Customer> customers)
        {
            if (this.GetCustomersAsyncCompleted != null)
            {
                this.GetCustomersAsyncCompleted(this, new GenericEventArgs<List<Customer>>(customers));
            }
        }

        public event EventHandler<GenericEventArgs<List<Customer>>> GetCustomersAsyncCompleted;

Erklärung:

Es gibt zwei öffentliche Methoden (GetCustomers & GetCustomersAsync) und ein Event (GetCustomersAsyncCompleted)  und eine private Methode (OnGetCustomersAsyncCompleted).

Wer noch nie mit Events gearbeitet hat, der kann sich auch mein HowTo durchlesen.

Die Methode "GetCustomers" macht unseren Fake Datenbankzugriff (daher dort das Thread.Sleep um eine Verzögerung bei der Verbindung zu zeigen) und gibt die Daten zurück. Diese Methode ist synchron – wenn ein Client diese direkt so aufruft, dann blockiert im schlimmsten Fall der GUI Thread.

Die Methode "GetCustomersAsync" ist der asynchrone Gegenpart. Beachtet die Naming-Convention, asynchrone Methoden müssen immer mit einem "Async" gekennzeichnet werden und ein Event besitzen, welches mit "Completed" aufhört.

In der "GetCustomersAsync" Methode nutze ich den ThreadPool um mir diese Arbeit abzunehmen. Ich rufe in meinem Beispiel einfach die synchrone Version auf und gebe das Ergebnis einer privaten Methode "OnGetCustomersAsyncCompleted" auf. Anstatt des ThreadPools hätte ich mir auch direkt einen neuen Thread erstellen können oder eine andere der unzähligen Möglichkeiten aussuchen können. Der Syntax sieht etwas kurios aus, weil ich eine annonyme Methode benutze.

Diese Methode prüft ob sich irgendjemand auf das Event registriert hat, wenn ja, wird das Event abgefeuert.

Damit die Daten auch entsprechen ankommen, habe ich mir eine Hilfklasse geschrieben, die "GenericEventArgs" – so brauche ich die EventArgs nicht casten, sondern es ist streng typisiert:

    public class GenericEventArgs<TValue> : EventArgs
    {
        public GenericEventArgs(TValue args)
        {
            this.EventArgs = args;
        }

        public TValue EventArgs { get; internal set; }
    }

Program.cs:

In unserer Program.cs nutzen wir nun diesen Code:

        static void Main(string[] args)
        {
            CustomerRepository rep = new CustomerRepository();

            Console.WriteLine("Sync: A | Async: B");
            string choice = Console.ReadLine();

            if(choice.ToLower() == "a")
            {
                DisplayCustomers(rep.GetCustomers());
            }
            if (choice.ToLower() == "b")
            {
                rep.GetCustomersAsyncCompleted += new EventHandler<GenericEventArgs<List<Customer>>>(rep_GetCustomersAsyncCompleted);
                rep.GetCustomersAsync();
            }

            Console.ReadLine();
        }

        static void rep_GetCustomersAsyncCompleted(object sender, GenericEventArgs<List<Customer>> e)
        {
            DisplayCustomers(e.EventArgs);
        }

        static void DisplayCustomers(List<Customer> customers)
        {
            Console.WriteLine("Finished:");
            foreach (Customer customer in customers)
            {
                Console.WriteLine("+++");
                Console.WriteLine("Customer Name: " + customer.Name);
                Console.WriteLine("Customer Address: " + customer.Address);
                Console.WriteLine("Customer ID: " + customer.Id.ToString());
            }
        }
    }

Als erstes erstellen wir uns unser CustomerRepository. Danach lass ich den Benutzer entscheiden ob er die Daten synchron oder asynchron laden möchte, wenn er die Daten synchron lädt wird die "GetCustomers" Methode aufgerufen und das Ergebnis in die "DisplayCustomers" hineingegeben.

Bei der asynchronen Variante registriert man sich auf das Event und ruft die "GetCustomersAsync" Methode auf. Wenn die Aktion durchgelaufen ist, wird das Event "GetCustomersAsyncCompleted" aufgerufen und im Eventhandler werden die Daten dann ebenfalls der "DisplayCustomers" übergeben.

Ergebnis:

Wenn man nun das Programm startet, kann man während des synchronen Aktion keine weiteren Buchstaben mehr eingeben. Bei der asynchronen Variante kann man die ganze Zeit Buchstaben eintippen (auch wenn diese nix machen).

Ich wollte noch ein WinForms oder WPF Beispiel beilegen, allerdings kann man dort nicht einfach aus einem anderen Thread in die GUI reinschreiben (siehe "Probleme"). Dies geht nur in einer Konsolenanwendung.

Das ist aber ein HowTo für später ;)

[ Download Democode ]


Kick It auf dotnet-kicks.de
Wenn dir der Blogpost gefallen hat, dann hinterlasse doch einen Kommentar. Wenn du auf dem Laufenden bleiben willst, abonniere unseren RSS Feed oder folge uns auf Twitter.

About the author

Written by Robert Mühsig

Robert Mühsig (@robert0muehsig) ist Webentwickler und beschäftigt sich mit Web-Frameworks (vor allem dem ASP.NET MVC Framework) und scheut sich auch nicht vor Javascript. Ansonsten bloggt er über all jene Probleme, die ihm über den Weg laufen. Seit 2008 ist er Microsoft MVP für ASP.NET und er arbeitet bei der T-Systems Multimedia Solutions GmbH in Dresden. Treffen kann man ihn online via Twitter (@robert0muehsig) oder dieser Seite oder bei der .NET User Group Dresden.

6 Responses

  1. Hallo Robert,

    Wenn ich deinen Code richtig lese, registrierst du zwar den EventHandler, um die fertigen Daten zu verarbeiten, deregistrierst diesen aber nicht mehr. Wenn du die gleiche Aktion n-mal  aufrufst, wird die Verarbeitung der Daten auch n-mal angestoßen. Im Handler rep_GetCustomersAsyncCompleted sollte sich dieser daher wieder vom Event rep.GetCustomersAsyncCompleted deregistrieren.

    Gut, dein Programm wird nach einem Durchlauf beendet (und damit auch die Instanz von CustomerRepository freigegeben), allerdings halte ich diesen Punkt bei dem von dir vorgestellten Ansatz für essentiell wichtig und er sollte daher erwähnt werden. Wenn die Klasse, die die Daten beschafft, länger als für die reine Datenbeschaffung instanziiert bleibt (z.B. bei der Verwendung eines Singleton), muss dafür gesorgt werden, dass der Handler für das Event wieder deregistriert wird.

    Grüße und schöne Restostern

    Reply
  2. Hallo Peter,
    vollkommen richtig – der Code in der Program.cs ist nicht sonderlich schön und sollte so auf keinen Fall in einem größeren Projekt eingesetzt werden.
    Ich brauchte nur schnell einen Client der mein Repository benutzt – das ging am schnellsten und sollte auch nur das Prinzip zeigen.
    In größeren Projekten würde ich es ebenso machen, wie du vorschlägst.
    Grüße und dir ebenfalls noch schöne Ostern.

    Reply
  3. Hey

    Hi habe auch einen Seite gefunden die eine sehr gute Erklärug hat.
    Eine sehr gute Installationsanleitung mit schönen großen SCreenshots ist
    vorhanden.
    Das ganze kann man sich dann auch als PDF herunterladen (und dann
    ausdrucken), bietet sich auch an wenn man nur einen Pc hat.
    Ihr findet die Seite hier:
    http://backtrack.1rss.de
    Es handelt sich dabei um die Installation von Backtrack 4 Pre.
    Andere News zu Linux sind dort auch zu finden.
    Bei mir hat die installation super geklappt.

    MFG
    Machs gut

    Monika

    Reply

Comment on this post

Letzte Posts

  • image.png
    RavenHQ–RavenDB in der Cloud

    Ayende Rahien hat es heute verkündet – RavenHQ, der RavenDB Cloud Hoster (natürlich von und mit Ayende) ist ab heute raus aus der Beta und man kann es von überall aus nutzen. In der Betaphase waren nur Nutzer von AppHarbor zugelassen. Was ist RavenHQ? RavenHQ ist im Grunde ein gehostes RavenDB in den Rechenzentren von ...

  • image.png
    GitHub for Windows–erste Eindrücke

    Git ist schon eine tolle Sachen und eröffnet viele neue Möglichkeiten – allerdings ist der Einstieg recht hart und selbst wenn man die guten Hilfsanleitungen auf GitHub befolgt, kommt man am Anfang nur langsam vorwärt. Insbesondere ist das Tooling für Windows / .NET Entwickler auch nicht gerade “bekanntes Terrain”. GitHub to the rescue! Die GitHub ...

  • image.png
    Chocolatey–apt-get für Windows

    Durch Zufall bin ich auf das Tool “Chocolatey” gestoßen. Wer die Website sich anschaut, wird evtl. eine Verwandschaft mit NuGet ausmachen. Was macht Chocolatey? Chocolatey ist ein “Maschine Package Manager”, das bedeutet, dass man für seine Maschine einfach Tools runterladen und Updaten kann – direkt über die Konsole. Was ist der Unterschied zu NuGet? NuGet ...

  • image.png
    SASS, LESS & Coffeescript in Visual Studio mit der Web Workbench

    CSS und Javascript sind die “kleinste” Schnittmenge von allen Browsern für die Erstellung von Web-Applikationen. Leider geht dabei etwas komfort verloren, daher lieben alle Webentwickler jQuery! SASS und LESS sind zwei Varianten, wie man “schöner” CSS schreiben kann und Coffeescript versucht Javascript Entwicklung zu vereinfachen. Aber immer der Reihe nach… Was ist SASS? SASS steht ...

  • image.png
    Code-Inside Sample nun auf GitHub: Google Code zu GitHub Migration

    Seit einiger Zeit habe ich Beispielcode auf Google Code bereitgestellt. Einfach nur noch weg von Google Code O-Ton damals war: Ich hatte mich für Google Code entschieden, weil ich hoffe dass früher oder später die Google Code Suche nutzbar ist und es dadurch wenigstens ein kleiner Mehrwert entsteht. Allerdings wirft es momentan noch ein Fehler. ...

Auf Amazon einkaufen & unterstützen

Facebook