12 April 2009 Async, HowTo, Multithreading Robert Muehsig

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 ]


Written by Robert Muehsig

Software Developer - from Dresden, Germany, now living & working in Switzerland. Microsoft MVP & Web Geek.
Other Projects: KnowYourStack.com | ExpensiveMeeting | EinKofferVollerReisen.de

If you like the content and want to support me you could buy me a beer or a coffee via Litecoin or Bitcoin - thanks for reading!