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

  • Carriage Return / Neue Zeile in Textareas

    Eine kleine Aufgabe: Jede neue Textzeile (Carriage Return/Wenn man Enter drückt ) in einer Textarea soll ein Element in einer Auflistung sein – wie mach ich das jetzt am einfachsten? Eigentlich ein grundlegendes Element im Web und der Nutzer macht bewusst Absätze – daher wäre es nur gerecht, wenn man das auch entsprechend würdigt. Kleine ...

  • image.png
    Doom, Quake, Wolfenstein & co. Source Code auf GitHub

    id Software, die Macher von Doom, Quake, Wolfenstein & co., stellen regelmäßig ihre älteren Spieltitle als Open Source zur Verfügung. Das Ganze runterzuladen fand ich bisher immer recht mühselig, allerdings gibt es seit kurzer Zeit die Sourcen auch auf GitHub. Darunter Spiele wie Doom 3, Quake 3, Wolfenstein für iOS. Wer also schon immer mal ...

  • image.png
    Twitter Bootstrap 2.0 released & “Release Präsentation”

    Wie bereits vom Twitter Bootstrap Team angekündigt wurde offiziel die Version 2.0 des UI Toolskits “Twitter Bootstrap” veröffentlich. Zudem wurden die Slides, welche bei der Release Party gezeigt wurden auch veröffentlicht: Downloads finden sich auf der Twitter Bootstrap Seite auf GitHub. Wenn dir der Blogpost gefallen hat, dann hinterlasse doch einen Kommentar. Wenn du auf ...

  • image.png
    Javascript zu Dart Translator

    Dart, Google Javascript Alternative, wurde vor ein paar Monaten vorgestellt und die Webentwickler Szene ist noch etwas gespalten, ob Dart nun überflüssig ist oder einfach nur cool und längst überfällig ist. Um die Sprache näher zu erläutern hat Google die grundlegenden Javascript Basics nach Dart übersetzt. Das Ergebnis ist der “Translator”. Der Name mag momentan ...

  • Twitter Bootstrap 2.0–“Beta”

    Twitter Bootstrap, ein UI-Toolkit für Web-Applikationen von Twitter, erscheint (wie bereits berichtet) demnächst in der Version 2.0. Der offizielle Release ist am 31. Januar, allerdings beginnt jetzt laut Mark Otto (einer der Hauptentwickler von Twitter Bootstrap) die intensive Test-Phase. Das heisst, das es nun offiziel auch die 2.0 Dokumentation online gibt. Im Vergleich zur aktuellen ...

Support us!

Facebook