Dienstag, 2. November 2010

Multithreading leicht gemacht...

Neulich ist mir eine sehr schlanke Metode bewusst geworden, mehere Aufgaben parallel anzustoßen und diese am Ende einer Methode wieder zu synchronisieren..

Ein Szenario dazu: in einer Methode möchte ich drei parallele Aufgaben anstoßen. Während diese laufen habe ich selber noch ein bisschen Arbeit zu erledigen. Am Ende der Methode brauche ich dann die Ergebnisse der parallelen Aktionen.

Die erste Idee dazu sind natürlich Threads. Also, wie war das nochmal... Thread-Objekt, da kommt ein ThreadStart (oder ParameterizedThreadStart) rein. Mh... der nimmt als Thread-Code aber nur ein Action-Delegate. Ich wollte aber eigenltich auch eine Funktion ausführen, oder eine Action mit mehreren Parametern. Kriegt man natürlich alles hin - Wrapper-Methoden, mehrere Parameter in eine einzelnes Objekt verpacken und so weiter. Unter dem Strich sind solche Lösungen aber immer etwas unhandlich.

Die weitaus einfachere Lösung basiert intern natürlich auch auf Threads. Diese erstelle ich aber nicht selber. Sondern ich nutze einfach das Async-Pattern. Silverlight-Entwicklern dürfte dieses Muster schon begegnet sein, weil alle Serviceaufrufe auf diesem Muster basieren. Einen ausgezeichneten Blogpost zum Thema gibt es hier.

Folgende Punkte am Async-Pattern sind für die Lösung interessant:
  1. Man benötigt ein Delegate für seine Methode. Das Delegate stellt die Methoden Begin- / EndInvoke bereit. Die Standard Func / Action Delegates dürften für die meisten Fälle ausreichen.
  2. Man muss BeginInvoke kein Callback übergeben, der Parameter darf null sein.
  3. Bei Verwendung generischer Action- / Func-Delegates verlangt die BeginInvoke-Methode typsichere Parameterwerte.
  4. EndInvoke ist eine blockierende Methode - falls noch kein Ergebnis vorliegt wartet die Methode solange bis das der Fall ist.
Besonders interessant sind aus meiner Sicht die Punkte 2 und 4 in Kombination.

Ein bisschen Code sagt mehr als tausend Worte, also:

class Program
  {
    static void Main(string[] args)
    {
      // Delegates holen
      Func<long> CountAFunc = CountA;
      Action<int> WriteCAction = WriteC;

      // Asynchron aufrufen - keine Callbacks, wir wollen die Ergebnisse gleich hier haben.
      IAsyncResult CountAAsyncResult = CountAFunc.BeginInvoke(null, null);
      IAsyncResult WriteCAsyncResult = WriteCAction.BeginInvoke(100, null, null);

      // Selber was arbeiten
      Thread.Sleep(10 * 1000);

      // Auf asynchrone Ergebnisse warten - EndInvoke ist blockierend!
      long CountAResult = CountAFunc.EndInvoke(CountAAsyncResult);
      WriteCAction.EndInvoke(WriteCAsyncResult);

      // Ergebnisse ausgeben
      Console.WriteLine("CountAResult " + CountAResult);
      Console.ReadLine();
    }

    public static long CountA()
    {
      long Result = 0;

      for (int i = 0; i < 1000; i++)
      {
        Result += i;
        Console.WriteLine("A " + i);
        Thread.Sleep(10);
      }

      return Result;
    }

    public static void WriteC(int Start)
    {
      for (int i = Start; i < 1000; i++)
      {
        Console.WriteLine("C " + i);
        Thread.Sleep(10);
      }

    }
  }


Ich habe zweieinfache Methoden die asynchron ausgeführt werden sollen: CountA und WriteC. Erstere ist eine parameterlose Function die (umständlich) die Summe der Zahlen von 1 - 999 berechnet. Die zweite Zählt von einem Startwert bis 1000. Beide Methoden erzeugen noch eine Ausgabe auf der Konsole.

Zu den beiden Methoden beschafft man sich Delegates, ruft diese mit BeginInvoke auf. Dann erledigt man seine eigene Arbeit (hier einfach 10 sek. warten). Dann sammelt man mit EndInvoke auf den Delegates die tatsächlichen Ergebnisse ein und gibt diese aus.

Mulitthreading leicht gemacht! Mehr zum Thema gibt es in der MSDN.

Viel Spaß!