## 11.2 Multithreading mit der Klasse »Thread« ### 11.2.1 Einführung in das Multithreading Stellen Sie sich eine beliebige Anwendung vor, die in der Lage ist, Mails zu versenden. Das Versenden kann, wenn die Mail mehrere größere Anhänge hat, durchaus einen längeren Zeitraum in Anspruch nehmen. Während des Versendens wird eine Sanduhr angezeigt, und der Anwender kann nicht mit der Applikation weiterarbeiten. Erstrebenswert ist es, dem Anwender eine Anwendung auszuliefern, die ohne eine Verzögerung auskommt. Dazu wäre nur eine Applikation in der Lage, die das Multithreading beherrscht. Mit einer solchen Anwendung könnte der Benutzer weiterarbeiten, während (scheinbar) gleichzeitig die Mail verschickt wird. Die Entgegennahme der Benutzereingabe und das Versenden der Mail sind zwei Operationen, die in einer multithreading-fähigen Anwendung voneinander unabhängig sind und innerhalb eines einzigen Prozesses ablaufen. Beide Operationen können dabei dieselben Daten benutzen. Jeder Operation wird dazu ein eigener Thread zugeordnet. Ein Thread ist eine Ausführungseinheit und besteht aus einer kontinuierlichen Abfolge von Anweisungen. Sie werden weiter unten in den Beispielen noch sehen, wie das zu verstehen ist. Jeder gestarteten Anwendung ist ein Prozess zugeordnet, in dem mindestens ein Thread existiert. Somit ist ein Thread die kleinste Ausführungseinheit und gehört im Umkehrschluss grundsätzlich immer zu einem Prozess. Wird der letzte Thread eines Prozesses zerstört, wird die Laufzeitumgebung der Anwendung beendet. Wenn man eine Ein-Prozessor-Maschine voraussetzt, kann zu einem gegebenen Zeitpunkt nur ein Thread von der CPU bearbeitet werden. Stehen mehrere Threads derselben oder auch unterschiedlicher Anwendungen zur Ausführung in einer Warteschlange, erfolgt der Austausch der Threads in der CPU in sehr kleinen Zeitintervallen (Standard: 20 ms). Für die Zuteilung der CPU ist eine Komponente des Systems zuständig, der Scheduler. Einem Anwender fällt das nicht auf – aus seiner Sicht erscheint es so, als würde die Ausführung gleichzeitig erfolgen. Im Kontext jedes Threads sind alle Informationen enthalten, um die unterbrochene Ausführung zu einem späteren Zeitpunkt wieder problemlos aufnehmen zu können. Dazu gehören die Inhalte der CPU-Register zu dem Zeitpunkt, zu dem der Thread den Prozessor verlassen hat, sowie alle Informationen, die den Zustand des Threads beschreiben. ### 11.2.2 Threadzustände und Prioritäten #### Wartende, bereite und laufende Threads Jeder Thread kann sich in einem von drei möglichen Zuständen befinden: wartend (waiting) bereit (ready) laufend (running) Ein laufender Thread befindet sich aktuell im Prozessor und wird ausgeführt. Ein Prozessor kann zu einem gegebenen Zeitpunkt immer nur einen Thread bearbeiten. Nach Ablauf der zugestandenen Zeitspanne muss der laufende Thread die CPU räumen. Er reiht sich wieder in die Warteschlange auf der Zeitscheibe ein und hofft darauf, dass ihm möglichst schnell wieder Prozessorzeit zugeteilt wird. Die Threads, die in der Warteschlange stehen, werden als bereit bezeichnet. Nur einem bereiten Thread kann ein Zeitquantum der CPU zugestanden werden. Es gibt aber auch Threads, die während ihrer Ausführung freiwillig den Prozessor räumen und auch danach zunächst nicht mehr willens sind, sich in die Warteschlange der bereiten Threads einzuordnen. Diese Threads werden als wartend bezeichnet. Ein wartender Thread muss den Anstoß von einem anderen Thread bekommen, um in den bereiten Zustand überführt zu werden. Bezogen auf einen Prozessor kann sich zu einem gegebenen Zeitpunkt nur ein Thread im laufenden Zustand befinden. Die anderen Threads sind entweder bereit oder wartend. Der aktuell ausgeführte Thread muss unter folgenden Bedingungen den Prozessor aufgeben: Das ihm zugestandene Zeitquantum ist abgelaufen. Der Thread muss auf ein anderes Objekt oder eine Benachrichtigung warten. Er tritt in den Zustand wartend ein. Ein anderer Thread mit einer höheren Priorität befindet sich in der Warteschlange. Während die beiden zuerst aufgeführten Punkte aufgrund der vorherigen Ausführungen einleuchtend sein sollten, ist der dritte neu. #### Thread-Prioritäten Jedem Thread wird eine bestimmte Priorität zugeordnet. Stehen mehrere Threads in der Warteschlange, erhält derjenige Thread Prozessorzeit, dessen Priorität am höchsten ist. Windows kennt die Prioritätsstufen 1 bis 31, die allerdings nicht alle genutzt werden. Viele Systemthreads laufen mit einer höheren Priorität als die Threads »normaler« Anwendungen. Der Prioritätszuordnung eines Threads und den daraus resultierenden Konsequenzen bei der Prozessorzuteilung kommt eine sehr große Bedeutung zu. Gäbe es die Zuordnung einer Priorität nicht, wäre es unter anderem nicht möglich, aus einer laufenden Windows-Anwendung heraus eine andere zu aktivieren. Das System fängt nämlich in einem Thread hoher Priorität den Mausklick auf das Fenster einer inaktiven Anwendung ab, ordnet die Mauszeigerposition dem direkt darunter liegenden Fenster zu und aktiviert es. Die Priorität eines Threads kann bei Bedarf erhöht werden. Damit kann man erreichen, dass Aufgaben, die Vorrang vor anderen haben sollen, nicht gleichberechtigt mit den anderen Threads behandelt werden, sondern bevorzugt. Umgekehrt kann die Priorität eines Threads auch verringert werden, um ihn einerseits bereitzuhalten, ihn aber andererseits nur in bestimmten Situationen zur Ausführung zu bringen, möglicherweise auch erst nach vorheriger Erhöhung der Priorität. Einen Thread mit niedriger Priorität haben Sie schon kennengelernt: Es ist der Thread des Garbage Collectors. Dieser erhält erst dann Prozessorzeit, wenn keine andere Ausführungseinheit die CPU beansprucht oder die Ressourcen knapp werden. Tritt die letztgenannte Situation ein, wird die Priorität des Garbage Collectors angehoben, damit die Speicherbereinigung ihre Arbeit verrichten kann. Ein Thread, dessen Zeit auf der Zeitscheibe abgelaufen ist, wird als bereit markiert und sichert den Zustand der Daten im Stack. Danach sucht das Betriebssystem nach dem Thread, der sowohl als bereit markiert ist als auch gleichzeitig die höchste Priorität besitzt. Befinden sich mehrere bereite Threads auf derselben Prioritätsstufe, weist der Scheduler dem in der Reihenfolge nächsten Thread den Prozessor zu. ### 11.2.3 Zusammenspiel mehrerer Threads Eine multithreading-fähige Anwendung zu schreiben ist nicht einfach und birgt immer die latente Gefahr von Fehlern. Eine der größten Fehlerquellen ist der gleichzeitige Zugriff mehrerer Komponenten auf eine gemeinsame Ressource. Wartet zum Beispiel ausnahmslos jede der beteiligten Komponenten auf die Antwort der anderen, wäre ein Deadlock die Folge – die Anwendung kann nicht mehr weiterarbeiten und hängt sich auf. Wir können in diesem Kapitel nicht alle Aspekte erörtern, die in diesem Zusammenhang von Bedeutung sind. Sie sollten aber immer daran denken, dass mit steigender Anzahl der Threads das Gefahrenpotenzial exponential steigt, die Komplexität der Anwendung drastisch zunimmt und der Ablauf der Anwendung nur noch schwierig nachzuvollziehen ist. Trotz der Schwierigkeiten, die sich bei der Entwicklung einer Multithreading-Anwendung ergeben, gibt es eine Reihe von Situationen, in denen der Einsatz mehrerer Threads sinnvoll ist. Meistens handelt es sich um Operationen, die generell eine längere Zeitspanne für ihre Ausführung benötigen, z. B. bei der Kommunikation mit anderen Rechnern über das Netzwerk oder bei Tasks, die als zeitkritisch eingestuft werden müssen. ### 11.2.4 Die Entwicklung einer einfachen Multithreading-Anwendung Im folgenden Beispiel wird auf einfachste Weise neben dem Hauptthread, der beim Starten einer Anwendung automatisch erzeugt wird, ein zweiter Thread per Programmcode ins Leben gerufen. Anhand dieses kleinen Programms wollen wir uns mit den wichtigsten Grundlagen einer multithreading-fähigen Anwendung vertraut machen.
    // ------------------------------------------------------------
    // Beispiel: ...\Kapitel 11\EinfacherThread
    // ------------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        ThreadStart del;
        del = new ThreadStart(TestMethod);
        Thread thread = new Thread(del);
        // zweiten Thread starten
        thread.Start();
        for(int i = 0; i <= 100; i++) {
          for(int k = 1; k <= 20; k++)
            Console.Write(".");
          Console.WriteLine("Primär-Thread " + i);
        }
        Console.ReadLine();
      }
      // Diese Methode wird in einem eigenen Thread ausgeführt.
      public static void TestMethod() {
        for(int i = 0; i <= 100; i++) {
          for(int k = 1; k <= 20; k++)
            Console.Write("X");
          Console.WriteLine("Sekundär-Thread " + i);
        }
      }
    }
Alle Klassen, die mit der Entwicklung multithreading-fähiger Anwendungen unter .NET in Zusammenhang stehen, sind im Namespace System.Threading zu finden, der am Anfang des Programms mit using bekannt gegeben werden sollte. Die wichtigste Klasse innerhalb dieses Namespace dürfte die Klasse Thread sein, mit der ein neuer Thread erzeugt wird. Werfen wir einen Blick auf den eingesetzten Konstruktor dieser Klasse:
    public Thread (ThreadStart start);
Bei dem Parameter vom Typ ThreadStart handelt es sich um ein Delegate, das die Methode angibt, deren Anweisungen in einem neuen Thread ausgeführt werden sollen. Die Definition dieses Delegates lautet wie folgt:
    public sealed delegate void ThreadStart ();
Die Instanz eines Delegates kapselt den Zeiger auf die Speicheradresse einer Methode. Die Typen der Parameterliste des Delegates müssen den Typen der Parameterliste der Methode entsprechen, auf die das Delegate verweist. Demzufolge kann man dem Konstruktor der Klasse Thread über das Delegate nur die Adresse einer parameterlosen Methode zuweisen – in unserem Beispiel ist es die Methode TestMethod:
    ThreadStart del = new ThreadStart(TestMethod);
    Thread thread = new Thread(del);
Im ersten Schritt wird die Variable del vom Typ des Delegates ThreadStart deklariert. Dem Delegate wird die Adresse der benutzerdefinierten Methode übergeben. Danach kann die Thread-Klasse unter Übergabe der Referenz des Delegates instanziiert werden. Mit
    Thread thread = new Thread(new ThreadStart(TestMethod));
können Sie den Code auch einzeilig formulieren, da die Referenz auf das Delegate nicht mehr benötigt wird. Die Instanziierung der Thread-Klasse ist noch nicht ausreichend, um den zweiten Thread der Anwendung zu aktivieren. Entscheidend ist vielmehr die Methode Start des Thread-Objekts:
    thread.Start();
Mit dem Start der Anwendung wird bereits der erste Thread, der Primärthread, automatisch erstellt. Der zweite Thread wird erst durch den Aufruf von Start auf die Thread-Referenz zum Leben erweckt. In dem neuen Thread wird TestMethod ausgeführt. Beide Threads arbeiten zwei verschachtelte Schleifen ab. Die Schleifen sind so konstruiert, dass eine Zeitscheibeneinheit nicht ausreicht, um jeweils vollständig die Schleifen zu durchlaufen, denn dann könnten wir den Effekt des Multithreadings an der Konsole nicht erkennen. In der inneren Schleife wird eine Ausgabe in die Konsole geschrieben. Beim Primärthread handelt es sich um 20 Punkte pro Schleifendurchlauf, beim Sekundärthread sind es jeweils 20-mal der Buchstabe »X«. daran schließt sich noch die Angabe an, welcher Thread für die Ausgabe verantwortlich ist. Darüber hinaus wird der aktuelle Zählerstand der äußeren Schleife angehängt. Hier klicken, um das Bild zu vergrößern Abbildung 11.1 Die Ausgabe der Anwendung »EinfacherThread« Schauen wir uns nun die Ausgabe an, die – abhängig von der Hardware-Ausstattung, der Systemkonfiguration und anderen laufenden Anwendungen – durchaus anders aussehen kann. Die Interpretation der Ausgabe hilft, die Arbeitsweise der Threads im Zusammenhang mit der Zeitscheibe und der quasiparallelen Ausführung zu verstehen. Auch wenn die Anzeige ziemlich chaotisch anmutet, am Ende werden beide Threads ihre Aufgabe vollständig erledigt haben. #### Das Delegate »ParameterizedThreadStart« Das Delegate ThreadStart, mit dem die in einem separaten Thread laufende Methode beschrieben wird, haben wir im vorigen Abschnitt behandelt. ThreadStart hat jedoch ein Manko, denn diese Methode muss parameterlos sein. Manchmal ist es aber notwendig, der Threadmethode Daten zu übergeben. Dazu wird uns eine Alternative mit dem Delegate ParameterizedThreadStart geboten:
    public sealed delegate void ParameterizedThreadStart(object obj);
Die Instanz eines solchen Delegates kapselt den Zeiger auf eine Methode, die die Referenz auf ein beliebiges Objekt erwartet. Hier werden uns alle Türen geöffnet, denn wir können, falls mehrere Daten an die Methode übergeben werden sollen, auch ein Array oder eine Auflistung angeben. Die Erzeugung des Threads erfolgt in bekannter Weise. Der einzige Unterschied ist im Konstruktor der Klasse Thread zu finden, dem wir eine Instanz des Delegates vom Typ ParameterizedThreadStart übergeben:
    Thread thread = new Thread(new ParameterizedThreadStart(ThreadMethod));
Um die gewünschten Daten an die Threadmethode zu leiten, greifen wir auf eine Überladung der Start-Methode zu, der wir das entsprechende Argument mitteilen:
    thread.Start(IrgendEinObjekt);
Das übergebene Objekt enthält die Daten, die von der vom Thread ausgeführten Methode verwendet werden sollen. ### 11.2.5 Die Klasse »Thread« #### Zugriff eines Threads auf sich selbst Ein Thread wird erzeugt, wenn die Klasse Thread unter Übergabe eines Delegates instanziiert wird. Dies stellt nicht die einzige Möglichkeit dar, sich die Referenz auf einen Thread zu besorgen. Wenn es beispielsweise notwendig ist, auf dem Hauptthread Operationen auszuführen, steht Ihnen diese Referenz explizit nicht zur Verfügung, da der Thread implizit beim Start der Anwendung erzeugt wird. Abhilfe schafft die statische Eigenschaft CurrentThread, die eine Referenz auf den aktuellen Thread liefert. Nehmen wir an, dass ein Thread seine eigene Priorität mit der Eigenschaft Priority erhöhen soll. Dann müssten Sie
    Thread.CurrentThread.Priority = Prioritätswert;
codieren, damit der Thread auf sich selbst zugreifen kann. Auf die Eigenschaft Priority kommen wir später zu sprechen. #### Einen Thread für eine bestimmte Zeitdauer anhalten Im Beispiel EinfacherThread wurde eine Schleife konstruiert, um eine kleine Zeitverzögerung zu erreichen. Ohne Schleife könnte es sein, dass die gesamte Schleife des ersten Threads bereits vollständig abgearbeitet ist, bevor der zweite Thread zum ersten Mal in seine eigene Schleife eintritt. Die Thread-Klasse bietet für solche Fälle mit der Methode Sleep eine bessere Alternative, um einen Thread für eine bestimmte Zeitdauer anzuhalten und damit die Ausführung zu verzögern. Die Methode ist statisch definiert und kann nicht auf eine bestimmte Threadinstanz aufgerufen werden. Der aktuelle Thread zieht sich damit selbst aus dem Verkehr. Sleep ist übrigens unabhängig von der Taktfrequenz des Computers. Wird Sleep die Zahl 0 übergeben, wird der Thread dazu veranlasst, auf den verbleibenden Rest seiner Ausführungszeit zu verzichten und die CPU für den nächsten anstehenden Thread frei zu machen. Er reiht sich danach sofort wieder in die Warteschlange ein. #### Sicheres Beenden eines Threads Mit der Methode Thread.Abort lässt sich ein Thread terminieren. Der Aufruf bewirkt, dass dieAusnahme ThreadAbortException in der Laufzeitschicht ausgelöst wird. Damit ist es möglich, die Methode ordnungsgemäß zu beenden, beispielsweise um dabei offene Ressourcen zu schließen. Dazu zunächst ein Beispiel. Diesmal wird die Routine, die in einem zweiten Thread ausgeführt wird, in einer eigenen Klasse definiert. Damit ändert sich grundsätzlich nichts, da dem Delegate nun die Adresse der Instanzmethode in der Klasse mitgeteilt wird.
    // ------------------------------------------------------------
    // Beispiel: ...\Kapitel 11\AbortThread
    // ------------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        Demo obj = new Demo();
        ThreadStart del = new ThreadStart(obj.TestMethod);
        Thread thread = new Thread(del);
        Console.WriteLine("Thread wird jetzt gestartet");
        // Sekundärthread starten
        thread.Start();
        Console.WriteLine("Thread ist gestartet");
        Thread.Sleep(200);
        // Der sekundäre Thread wird durch den Primärthread mit
        // der Methode Abort zerstört.
        thread.Abort();
        Thread.Sleep(100);
        if (thread.IsAlive)
          Console.WriteLine("Der Sek.-Thread lebt noch");
        else
          Console.WriteLine("Der Sek.-Thread ist aufgegeben");
        Thread.Sleep(5000);
      }
    }
    class Demo {
      public void TestMethod() {
        try {
          Console.WriteLine("Sek.-Thread gestartet.");
          // Die Schleife zwingt dem Thread eine länger andauernde Ausführung auf.
          for(int i = 0; i <= 100; i++) {
            Console.WriteLine("Sek.-Thread-Zähler = {0}", i);
            Thread.Sleep(50);
          }
        }
        catch (ThreadAbortException ex) {
          Console.WriteLine("Sek.-Thread/im Catch-Block");
        }
        finally {
          Console.WriteLine("Sek.-Thread/in Finally");
        }
        Console.WriteLine("Sek.-Thread/nach Finally");
        for (int i = 0; i <= 20; i++) {
          Console.Write(".");
          Thread.Sleep(50);
        }
      }
    }
Nach dem Instanziieren der Thread-Klasse wird der zweite Thread gestartet. Da wir die Abort-Methode testen wollen, müssen wir dafür sorgen, dass Abort nicht auf einen Thread trifft, der nicht mehr ausgeführt wird. Deshalb ist in ThreadExecution der Klasse Demo eine Schleife eingebaut, die eine längere Zeit für einen vollständigen Durchlauf benötigt. Die Zeit muss so lang angesetzt werden, dass Abort auf die sich noch in Arbeit befindliche Schleife trifft. Vor dem Aufruf von Abort wird der Primärthread zunächst mit Sleep gebremst, damit der Sekundärthread etwas Zeit zum Arbeiten hat. Nach dem Aufruf von Abort bekommt das System mit einem zweiten Sleep-Aufruf noch Zeit, den Sekundärthread endgültig zu beenden. Durch Auswertung der Eigenschaft IsAlive auf dem Sekundärthread wird festgestellt, ob dieser noch aktiv ist oder nicht. Würden wir dem Hauptthread keine Ruhepause gönnen, könnte eine falsche Aussage die Folge sein, da die if-Bedingungsprüfung vor der Aufgabe des Sekundärthreads durchgeführt wird, weil sich Abort und if innerhalb derselben Zeitscheibe befinden und der freigegebene Thread noch keine Möglichkeit erhalten hat, die Ausnahme auszulösen. Die zweite Schleife in der Methode ThreadExecution der Klasse ClassA soll ebenfalls eine länger andauernde Operation simulieren. An der Konsole erfolgt die folgende Ausgabe:
    Thread wird jetzt gestartet
    Thread ist gestartet
    Sek.-Thread gestartet
    Sek.-Thread-Zähler = 0
    Sek.-Thread-Zähler = 1
    Sek.-Thread-Zähler = 2
    Sek.-Thread-Zähler = 3
    Sek.-Thread/in Catch-Block
    Sek.-Thread/in Finally
    Der Sek.-Thread ist aufgegeben
Hier ergibt sich anscheinend ein Widerspruch zu der Aussage in Kapitel 9, »Fehlerbehandlung und Debugging«, dass die hinter finally stehenden Anweisungen ausgeführt werden: Der Aufruf von Abort löst die Exception ThreadAbortException aus, aber die zweite Schleife im Sekundärthread wird nicht mehr durchlaufen. Genau in diesem Punkt liegt das Besondere dieser Ausnahme, denn sie wird ausgelöst und auch abgefangen, aber die Anweisungen hinter dem Ende der Ausnahmebehandlung kommen nicht mehr zur Ausführung, da der Thread in diesem Moment bereits terminiert ist. Allerdings unterstützt die Laufzeitschicht abschließende Anweisungen in finally. Gegen das außerplanmäßige Beenden kann sich der betroffene Thread allerdings auch zur Wehr setzen. Dazu muss im catch-Block des Exceptionhandlers die statische Methode ResetAbort aufgerufen werden:
    ...
    catch (ThreadAbortException e) {
      Thread.ResetAbort();
      Console.WriteLine("Sek.-Thread/im Catch-Block");
      ...
    }
Wenn Sie diese Anweisung in den Programmcode des Beispiels einbauen, wird auch die zweite Schleife in ThreadExecution ausgeführt, und die bedingte Prüfung mit if führt zu dem Ergebnis, dass der Thread noch lebt – das allerdings auch nur, weil die zweite Schleife ebenfalls wieder eine längere Zeit in Anspruch nimmt oder der Thread nicht schon auf normalem Wege aufgegeben worden ist, bevor die Prüfung erfolgt. #### Abhängige Threads – die Methode »Join« Nun wäre die folgende Ausgangssituation vorstellbar: Der Primärthread beendet den Sekundärthread mit Abort und muss dabei sicherstellen, dass die Anweisungen im Sekundärthread zuerst vollständig abgearbeitet sind, bevor die nächste Anweisung im Primärthread ausgeführt wird. Solche Situationen können auftreten, wenn der Code des Primärthreads auf das ordnungsgemäße Beenden angewiesen ist. Das heißt aber auch, dass der Aufruf synchron erfolgen muss – dass also auf die quasi gleichzeitige Ausführung, die ansonsten die Threads auszeichnet, bewusst verzichtet wird. Wir wollen, um uns der Problematik bewusst zu werden, zunächst eine kleine Änderung in Main vornehmen. Die Implementierung der Klasse Demo bleibt wie im Beispiel AbortThread erhalten (also ohne den Aufruf von ResetAbort, falls Sie damit experimentiert haben sollten).
    static void Main(string[] args) {
      Demo obj = new Demo();
      ThreadStart firstThread;
      firstThread = new ThreadStart(obj.TestMethod);
      Thread TheThread = new Thread(firstThread);
      Console.WriteLine("Thread wird jetzt gestartet");
      // sekundären Thread starten
      TheThread.Start();
      Console.WriteLine("Thread ist gestartet");
      Console.WriteLine("vor Abort.............");
      // Der sekundäre Thread wird durch den Primärthread mit
      // der Methode Abort zerstört.
      Thread.Sleep(200);
      TheThread.Abort();
      // Die folgende Anweisung simuliert Code, der vom Beenden
      // des Sekundärthreads abhängig ist.
      Console.WriteLine("nach Abort.............");
      Thread.Sleep(100);
      if (TheThread.IsAlive)
        Console.WriteLine("Der Sek.-Thread lebt noch");
      else
        Console.WriteLine("Der Sek.-Thread ist aufgegeben");
      Thread.Sleep(5000);
    }
Main enthält eine Anweisung, die nach dem Aufruf der Abort-Methode die Konsolenausgabe
    nach Abort................
erzwingt. Damit sollen Anweisungen simuliert werden, die auf das ordnungsgemäße Terminieren des sekundären Threads angewiesen sind. Sehen wir uns die Konsolenausgabe des Programmcodes in Abbildung 11.2 an. Deutlich ist zu erkennen, dass der sekundäre Thread nach Abort immer noch aktiv ist – die catch- und finally-Blöcke werden nach der abhängigen Anweisung ausgeführt. Hier klicken, um das Bild zu vergrößern Abbildung 11.2 Abhängige Threads – unerwünschter Programmfluss Jetzt hilft eine andere Methode der Klasse Thread weiter: Join, die den aktuellen, also aufrufenden Thread so lange blockiert, bis der Sekundärthread vollständig terminiert ist. Sinnvollerweise wird Join direkt hinter Abort aufgerufen. Der Programmablauf kehrt erst dann zum Aufrufer zurück, wenn die Threadausführung ordnungsgemäß beendet ist.
    // ------------------------------------------------------------
    // Beispiel: ...\Kapitel 11\AbhängigerThread
    // ------------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        ...
        // Der sekundäre Thread wird durch den Primärthread mit
        // der Methode Abort zerstört.
        Thread.Sleep(200);
        TheThread.Abort();
        TheThread.Join();
        // Die folgende Anweisung simuliert Code, der vom
        // Beenden des Sekundärthreads abhängig ist.
        Console.WriteLine("nach Abort.............");
        ...
      }
    }
    ...
Wenn Sie dieses Programm starten, gibt die Konsole das Ergebnis aus Abbildung 11.3 aus. Wenn wir diese Ausgabe mit der vergleichen, die wir ohne Join hatten (Abbildung 11.2), können wir eindeutig erkennen, dass der Thread, dessen Terminierung angestoßen wurde, zuerst vollständig abgearbeitet wird, bevor der Aufrufer seinen eigenen Programmfluss fortsetzt. Hier klicken, um das Bild zu vergrößern Abbildung 11.3 Ausgabe nach dem sicheren Beenden des Threads #### Threadprioritäten festlegen Jeder Thread hat eine Priorität. Mit der Eigenschaft Priority lässt sich die Priorität eines Threads erhöhen, verringern oder einfach nur auswerten. Die Priorität spielt eine entscheidende Rolle bei der Vergabe der Zeitscheibe: Ein Thread hat Vorrang vor einem anderen Thread mit niedrigerer Priorität – vorausgesetzt natürlich, dass sich beide durch den Zustand bereit beschreiben lassen. Priority ist vom Typ der Enumeration ThreadPriority, die fünf Member definiert: Highest, AboveNormal, Normal, BelowNormal, Lowest. Die Prioritäten können von der höchsten Stufe (Threadpriority.Highest) bis zur niedrigsten (ThreadPriority.Lowest) eingestellt werden. Die automatisch einem Thread zugewiesene Priorität lautet Normal. Der Thread mit der höchsten Priorität erhält die Zeitscheibe und läuft so lange, bis er sich selbst mit Sleep einfriert, bis seine Operationen beendet sind, bis Abort auf ihm aufgerufen wird oder bis ein Thread höherer Priorität lauffähig ist und Anspruch auf die CPU erhebt. Am häufigsten ist der Fall anzutreffen, dass sich mehrere Threads gleicher Priorität in die Warteschlange zur CPU eingeordnet haben. Alle erhalten gleiche Zeitanteile der CPU nach einem Verfahren, das als Round-Robin-Verteilungsverfahren bezeichnet wird. Im folgenden Beispielprogramm wollen wir die Auswirkungen der Prioritätsfestlegung in einer Anwendung studieren.
    // -------------------------------------------------------------
    // Beispiel: ...\Kapitel 11\ThreadPriorität
    // -------------------------------------------------------------
    class Program {
      // Starten des primären Threads
      static void Main(string[] args) {
        Demo obj = new Demo();
        Thread thread1, thread2;
        thread1 = new Thread(new ThreadStart(obj.Execution1));
        thread2 = new Thread(new ThreadStart(obj.Execution2));
        // die Priorität von thread1 hochsetzen
        thread1.Priority = ThreadPriority.AboveNormal;
        // thread 1 starten
        thread1.Start();
        // thread 2 starten
        thread2.Start();
        Console.ReadLine();
      }
    }
    class Demo {
      public void Execution1() {
        for (int i = 0; i <= 500; i++) {
          Console.Write(".");
        }
      }
      public void Execution2() {
        for (int number = 0; number <= 10; number++)
          Console.WriteLine("It's me,Thread2");
      }
    }
Um den Unterschied deutlich zu machen, empfiehlt es sich, beim ersten Versuch die Anweisung zur Erhöhung der Priorität des ersten Threads auszukommentieren. Wenn Sie mit dieser Vorgabe die Laufzeitumgebung starten, werden Sie eine Konsolenausgabe erhalten (siehe Abbildung 11.4). thread1 wird gestartet, schreibt ein paar Punkte in die Ausgabe und übergibt danach dem Prozessor den thread2, der sich durch eine eigene Zeichenfolge bemerkbar macht. Die Zeitscheibe dauert lange genug, um die Anweisungen von thread2 vollständig zu bearbeiten. Danach übernimmt wieder thread1 die CPU und beendet seine Ausführung. Einem Thread eine gewisse Sonderstellung durch die Erhöhung der Priorität einzuräumen, mag vielleicht manchmal ganz verlockend klingen. Bedenken Sie jedoch, dass dieser Thread bei einer lange andauernden Operation eine bremsende Wirkung auf die anderen Threads hat. Man spricht auch von einem Aushungern des Systems. Gehen Sie daher sorgfältig mit dem Erhöhen von Prioritäten um, und achten Sie darauf, dass keine unnötigen Operationen von einem solchen Thread ausgeführt werden, sondern nur solche, die für den weiteren Ablauf der Anwendung unbedingt notwendig sind. Hier klicken, um das Bild zu vergrößern Abbildung 11.4 Threadpriorität #### Vorder- und Hintergrundthreads Threads werden in zwei Kategorien unterteilt: in Vorder- und in Hintergrundthreads. Ein Prozess wird ausgeführt, solange noch mindestens ein Vordergrundthread existiert. Mit dem Beenden des letzten Vordergrundthreads wird der Prozess der Anwendung selbst dann beendet, wenn Hintergrundthreads noch aktiv sind und die ihnen auferlegte Aufgabe noch nicht vollständig ausgeführt haben. Das Beenden eines Hintergrundthreads hat im umgekehrten Fall aber nicht zur Konsequenz, dass der Vordergrundthread beendet wird. Die Eigenschaft IsBackground beschreibt, ob ein Thread als Vorder- oder Hintergrundthread eingestuft ist. Grundsätzlich sind alle Threads, die aus der Klasse Thread erzeugt werden, zunächst Vordergrundthreads. Mit IsBackground lässt sich ein Thread aber auch zu einem Hintergrundthread degradieren. ### 11.2.6 Threadpools nutzen Die Arbeit mit mehreren Threads lässt sich durch Threadpools wesentlich vereinfachen, denn die Laufzeitumgebung erzeugt eine bestimmte Anzahl von Threads, wenn sie gestartet wird. Sie können diese Threads nutzen und brauchen nicht eigens neue zu erzeugen, wenn Sie welche benötigen. Nach der Beendigung einer Threadmethode wird der frei gewordene Thread in den Pool zurückgeführt und steht anderen Aufgaben zur Verfügung. Angesprochen wird der Threadpool mit der gleichnamigen Klasse Threadpool. Mit deren statischer Methode QueueUserWorkItem wird der Threadpool aktiviert. Dabei wird der Methode ein Delegate vom Typ WaitCallback übergeben, das die Methode beschreibt, die mit dem Thread ausgeführt werden soll. Grau ist alle Theorie, daher sehen wir uns zuerst ein komplettes Beispiel an:
    // --------------------------------------------------------------
    // Beispiel: ...\ Kapitel 11\ThreadpoolSample
    // --------------------------------------------------------------
    class Program {
      static void Main(string[] args) {
        // den Threadpool erforschen
        int maxThreads;
        int asyncThreads;
        ThreadPool.GetMaxThreads(out maxThreads, out asyncThreads);
        Console.WriteLine("Max. Anzahl Threads: {0}", maxThreads);
        Console.WriteLine("Max. Anzahl E/A-Threads: {0}", asyncThreads);
        Console.WriteLine(new string('-', 40));
        // Benachrichtigungsereignis, Zustand 'nicht signalisieren'
        AutoResetEvent ready = new AutoResetEvent(false);
        // Anfordern eines Threads aus dem Pool
        ThreadPool.QueueUserWorkItem(new WaitCallback(Calculate), ready);
        Console.WriteLine("Der Hauptthread wartet ...");
        // Hauptthread in den Wartezustand setzen
        ready.WaitOne();
        Console.WriteLine("Sekundärthread ist fertig.");
        Console.ReadLine();
      }
      public static void Calculate(object obj) {
        Console.WriteLine("Im Sekundärthread");
        Thread.Sleep(5000);
        // Ereigniszustand auf 'signalisieren' festlegen
        ((AutoResetEvent)obj).Set();
      }
    }
Die Methode Calculate soll in einem Thread aus dem Threadpool ausgeführt werden. Bevor diese Operation eingeleitet wird, wollen wir aber noch feststellen, wie viele Threads uns der Pool zur Verfügung stellt, und rufen dazu die statische Methode GetMaxThreads auf. Über den ersten Parameter werden uns die Threads geliefert, der zweite Parameter gibt darüber hinaus Auskunft über die maximale Anzahl der möglichen E/A-Anforderungen. Sie werden feststellen, dass sich 25 Threads im Pool befinden, und zwar pro Prozessor. Das Beispiel ist so entwickelt, dass nicht nur ein Thread aus dem Pool zur Ausführung der Methode Calculate herangezogen wird. Darüber hinaus wird auch ein Synchronisationsszenario in Gang gesetzt, das bewirkt, dass während der Ausführung von Calculate der aufrufende Code in Wartestellung versetzt wird und auf ein Signal von Calculate wartet, bevor er seine Arbeit wieder aufnimmt. Mehr zur Synchronisierung erfahren Sie im folgenden Abschnitt. Dem Aufruf der statischen Methode QueueUserWorkItem wird ein Delegate übergeben, das die Methode beschreibt, die im Thread ausgeführt werden soll. Darüber hinaus kann QueueUserWorkItem ein zweites Argument übergeben werden, um der Threadmethode Daten bereitzustellen. Hier wird dem zweiten Parameter ein Objekt vom Typ AutoResetEvent übergeben. Dieses Objekt versetzt zwei Threads in die Lage, über Signale miteinander zu kommunizieren. Erzeugt wird das Objekt im Code mit:
    AutoResetEvent ready = new AutoResetEvent(false);
Der Übergabeparameter false besagt, dass der anfängliche Zustand des Objekts auf »nicht signalisiert« festgelegt wird. Mit
    ready.WaitOne();
wird der aktuelle Thread so lange blockiert, bis er ein Signal erhält. Dieses stammt aus der Threadmethode und wird durch Aufruf der Set-Methode des AutoResetEvent-Objekts ausgelöst:
    ((AutoResetEvent)obj).Set();
Hier profitieren wir davon, dass wir der Threadmethode im zweiten Parameter die Referenz auf das AutoResetEvent übergeben haben. ### 11.2.7 Synchronisation von Threads Solange nur ein Thread eine bestimmte Methode aufruft, hat man die Garantie, dass der Code von der ersten bis zur letzten Anweisung durchlaufen wird. Sind mehrere Threads im Spiel, könnte einer der Threads eine Methode in einem ungültigen Zustand hinterlassen, wenn das System ihm die Zeitscheibe quasi mitten in der Ausführung der Methode entzieht und der nächste Thread mit derselben Methode auf demselben Objekt zu arbeiten beginnt. Der Thread, der den Objektzustand von seinem Vorgänger übernommen hat, produziert dann möglicherweise Ergebnisse, die nicht vorhersehbar und in der Regel auch falsch sind. Hier kommt ein neuer Begriff ins Spiel, der Ihnen vielfach in der Dokumentation zur .NET-Klassenbibliothek begegnen wird: die Threadsicherheit.
Hinweis
Unter Threadsicherheit versteht man, dass ein Objekt auch dann in einem gültigen Zustand bleibt, wenn mehrere Threads gleichzeitig auf dieselbe Ressource zugreifen. Threadsicher bedeutet nichts anderes, als dass mehrere Threads gleichzeitig dieselbe Methode desselben Objekts aufrufen dürfen, ohne dass es zu Konflikten kommt.
#### Unsynchronisierte Threads Bevor wir uns mit den Details der Threadsicherheit beschäftigen, wollen wir uns an einem Beispiel verdeutlichen, was unter einem ungültigen Zustand zu verstehen ist und welche Auswirkungen das haben kann.
    class Program {
      static void Main(string[] args) {
        Demo obj = new Demo();
        Thread thread1, thread2;
        thread1 = new Thread(new ThreadStart(obj.Worker));
        thread2 = new Thread(new ThreadStart(obj.Worker));
        thread1.Start();
        thread2.Start();
        Console.ReadLine();
      }
    }
    class Demo {
      private int value;
      public void Worker() {
        while(true) {
          value++;
          if (value > 100) break;
            Console.WriteLine(value);
        }
      }
    }
Das Projekt enthält zusätzlich zu Main noch die Definition der Methode Worker in Demo. In Main werden zwei Threads konstruiert, die beide die Methode Worker aufrufen. Worker selbst durchläuft eine Schleife, in der die Variable value hochgezählt und der aktuelle Inhalt an der Konsole ausgegeben wird. Mit dem Endwert von 100 wird die Methode wieder verlassen. Beide Threads greifen auf dasselbe Objekt zu und teilen sich die Arbeit mehr oder weniger abwechselnd, um das Feld value hochzuzählen und dessen Inhalt anzuzeigen. Eigentlich sollte man erwarten, dass die Zahlen chronologisch hintereinander ausgegeben werden, jedoch kommt es an der Konsole beispielsweise zu folgender Ausgabe: 1, 2, 3, 4, … , 39, 41, 42, 43, …, 99, 100, 40. Beide Threads greifen unsynchronisiert auf die Variable value zu, wobei die Operation des ersten Threads mitten in der Schleife unterbrochen wird. Dieses ist dem Anschein nach genau der Moment, nachdem der Feldinhalt mit der Anweisung
    value++;
zwar schon auf 40 erhöht, aber mit
    Console.WriteLine(value);
noch nicht an der Konsole ausgegeben wurde. Der unterbrochene Thread weiß natürlich genau, mit welcher Anweisung er seine Arbeit wieder aufnehmen muss, wenn ihm der Scheduler wieder Prozessorzeit zuteilt: Er muss zuerst die Zahl 40 ausgeben. Diesen Zwischenstand, dessen Informationen durch den Inhalt der CPU-Register beschrieben werden, speichert das System im Stack und räumt daraufhin den Prozessor für den nächsten Thread in der Warteschlange. Der zweite Thread, dem anschließend die CPU zugeteilt wird, tritt nun seinerseits zum ersten Mal in die Schleife ein, erkennt den aktuell gültigen Feldinhalt der Variablen (er beträgt 40), erhöht diesen zunächst auf 41, gibt den Wert aus und setzt die Schleife so lange fort, bis seine Zeit abgelaufen ist. Dann verlässt der zweite Thread die CPU, das System liest die im Stack gesicherten Daten des ersten Threads in die CPU ein und setzt die Arbeit mit genau der Anweisung fort, bei der er unterbrochen wurde: mit der Ausgabe der Zahl 40 an der Konsole. ### 11.2.8 Der Monitor zur Synchronisation Beide Threads des Beispiels arbeiten ohne Synchronisation und hinterlassen ihrem Nachfolger die Ressource in einem ungültigen Zustand. Das wollen wir natürlich vermeiden – die Feldinhalte sollen so ausgegeben werden, dass sie dem tatsächlich aktuellen Stand des Feldes entsprechen. Wenn wir die Arbeitsweise der Methode Worker analysieren, kommen wir zu der Feststellung, dass ein ganz bestimmter Codeteil als kritisch angesehen werden kann. Es sind die beiden Anweisungen:
    value++;
    Console.WriteLine(value);
Die Ausgabe wird nur dann unseren Erwartungen entsprechen, wenn ein laufender Thread seine Ausführung nicht zwischen diesen beiden Anweisungen unterbrechen muss, denn zur Erhöhung des Feldwertes gehört auch die Anzeige an der Konsole. Dieser Zusammenhang muss für jeden der beiden Threads ersichtlich sein. An dieser Stelle kommt eine neue Klasse ins Spiel, die die Aufgabe der Synchronisation übernimmt: Monitor. Mit dieser Klasse lässt sich verhindern, dass mehrere Threads gleichzeitig einen bestimmten Codeteil im Programm durchlaufen. Mit anderen Worten bedeutet das, dass zu einem Zeitpunkt immer nur ein Thread dieses Codesegment durchlaufen kann. Andere Threads, die ebenfalls dieses Codesegment ausführen wollen, müssen warten, bis der laufende Thread das Codesegment verlassen hat. Mit den Methoden Enter und Exit der Klasse Monitor können kritische Codeabschnitte definiert werden, die zu einem gegebenen Zeitpunkt nur von einem Thread betreten werden dürfen. Mit Enter wird das Codesegment so lange blockiert, bis die Sperrung mit Exit wieder aufgehoben wird. Damit sind ungültige Zustände, die ein Thread hinterlassen könnte, wenn ihm die Zeitscheibe entzogen wird, nicht mehr möglich. Monitor protokolliert, ob der Vorgängerthread den kritischen Abschnitt mit Exit ordnungsgemäß verlassen hat oder nicht. Sowohl Enter als auch Exit sind statische Methoden der Klasse Monitor. Als Argument wird den beiden Methoden die Referenz auf das zu synchronisierende Objekt übergeben, das auch this sein darf. Wir ändern jetzt das Beispiel oben und schaffen die Voraussetzung dafür, dass die Zugriffe auf die kritischen Anweisungen synchronisiert erfolgen. Zur Bestätigung lassen wir uns diesmal zusätzlich noch den Hashcode des jeweiligen Threads ausgeben, der die angezeigte Zahl erzeugt hat.
    // -------------------------------------------------------------
    // Beispiel: ...\Kapitel 11\SynchronisierteThreads
    // -------------------------------------------------------------
    ...
    class Demo {
      private int value;
      public void Worker() {
        while(true) {
          // Sperre setzen
          Monitor.Enter(this);
          value++;
          if (value > 100) break;
          Console.WriteLine("Zahl = {0,5} Thread = {1,3}", value,
                            Thread.CurrentThread.GetHashCode().ToString());
          Thread.Sleep(5);
          // Sperre aufheben
          Monitor.Exit(this);
        }
      }
    }
Nun erhalten wir wunschgemäß die Ausgabe der chronologisch geordneten Zahlen von 1 bis 100. Neben der Enter-Methode gibt es in der Monitor-Klasse noch die Methode TryEnter. Diese überprüft zuerst, ob der geschützte Codeabschnitt frei ist, sperrt ihn dann und führt den Code aus. Ist das Codesegment gesperrt, liefert TryEnter den Rückgabewert false. Darauf kann der Entwickler entsprechend reagieren. Der Rückgabewert kann beispielsweise von einem if-Statement sinnvoll ausgewertet werden. #### Das »lock«-Statement Neben Enter und Exit der Klasse Monitor gibt es noch eine andere, sprachspezifische Möglichkeit, den Zugriff zu synchronisieren. Unter C# ist das die l ock-Anweisung. Die Syntax dazu lautet:
    lock (object obj)
    {
      // zu synchronisierende Anweisungen
    }
Durch den Anweisungsblock hinter lock werden die Anweisungen eingeschlossen, die es zu synchronisieren gilt. Dieses Statement ist sehr einfach zu handhaben, aber es besitzt nicht die Möglichkeiten, mit denen die Klasse Monitor ausgestattet ist. #### Die Methoden »Wait« und »Pulse« Die Klasse Monitor ist nicht instanziierbar, da jedem Objekt nur ein Monitor zugeordnet werden kann. Mehrere Objekte können den Anspruch auf die Nutzung des Monitors eines anderen Objekts erheben, aber nur einem Objekt aus der Warteschlange wird er zugestanden. Stellen Sie sich den Monitor wie ein Fernglas vor, das Sie mit in den Urlaub genommen haben, um damit die Landschaft aus der Nähe zu betrachten. Solange Sie das Fernglas benutzen, hat keine andere Person die Möglichkeit, die schönen Dinge der Natur aus der Nähe zu betrachten. Eine andere Person, die auch einen Blick durch das Fernglas werfen möchte, wird sich in die Warteschlange einreihen müssen. Erst wenn Sie das Fernglas zur Seite gelegt haben, kann es von einer Person aus der Warteschlange aufgenommen werden. Alle anderen Personen müssen sich weiter gedulden. Nehmen wir jetzt an, Sie wären mit einem Ihrer Freunde im Urlaub. Während Sie durch das Fernglas schauen, erhebt auch Ihr Freund darauf Anspruch. Sie legen das Fernglas freiwillig zur Seite, informieren Ihren Freund darüber, dass er es nun benutzen darf, und treten freiwillig in die Warteschlange. Die letzten beiden Aktionen lassen sich auch auf den Monitor projizieren. Sobald Sie das Fernglas mit der Absicht zur Seite legen, es zu einem späteren Zeitpunkt noch einmal zu benutzen, versetzen Sie sich in den Wartezustand und begeben sich in die Warteschlange. Die Monitor-Klasse beschreibt diese Operation mit der statischen Methode Wait. Das Informieren des nächsten Interessenten in der Warteschlange entspricht der ebenfalls statischen Methode Pulse. Beide Methoden können nur innerhalb eines Synchronisationsblocks aufgerufen werden. Mit Wait wird der aktuelle Thread blockiert und gleichzeitig die Sperrung des Objekts aufgehoben. Damit kann ein anderer Thread das freigegebene Objekt nutzen. Schauen wir uns eine Definition der überladenen Wait-Methode an:
    public static bool Wait(object obj);
Der Parameter nimmt die Referenz auf das Objekt entgegen, dessen Sperrung aufgehoben werden soll. Ein wenig sonderbar verhält sich der Rückgabewert. Er ist true, wenn kein anderer Thread das Objekt sperrt und der aktuelle Thread selbst die Verantwortung der Sperrung übernimmt. Ansonsten kommt kein boolescher Wert zurück, was eine Einreihung in die Warteschlange zur Folge hat. Damit bietet sich Wait auch dazu an, als Bedingung für den Eintritt in eine Schleife behilflich zu sein:
    while(Monitor.Wait(obj)) {
      // Thread tritt in den synchronisierten Block ein
    }
Es besteht ein großer Unterschied zwischen einem Thread, der mit Enter auf den Eintritt in eine synchronisierte Methode wartet, und einem Thread, der sich mit Wait in den Wartezustand versetzt hat. Ein Thread, der eine synchronisierte Methode mit Enter betreten möchte, befindet sich im Zustand bereit. Er reiht sich in die Threads ein, die auf Anweisung des Schedulers hin ein Segment der Zeitscheibe erhalten. Ein Thread, der mit Wait die Sperrung eines Objekts aufgehoben hat, befindet sich in einer Warteliste – allerdings nicht in der Warteliste, aus der der Scheduler einem bereiten Thread die CPU zuteilt, sondern in einer Warteliste aller der Threads, die durch den Zustand wartend gekennzeichnet sind. Um einen Thread aus seinem Wartezustand zu holen, muss ein anderer Thread die Methode Pulse oder PulseAll auf dem gesperrten Objekt aufrufen. Das Problem ist, dass Pulse keinen bestimmten wartenden Thread aus der Liste holt, sondern – falls sich mehrere Threads darin befinden – einen mehr oder weniger willkürlich gewählten, während mit PulseAll alle Threads den Zustand wartend aufgeben und in bereit übergehen. Damit stehen sie wieder in der Warteschlange der Zeitscheibe – der Scheduler kann ihnen wieder Prozessorzeit zuteilen. Ein Thread, der mit Wait die Sperrung des kritischen Codebereichs aufgehoben hat, wartet auf einen Anstoß von außen, um wieder aktiv werden zu können. Er selbst hat keine Möglichkeit, diesen Zustand zu beenden. Wenn kein anderer Thread Pulse oder PulseAll aufruft, wird ein wartender Thread daher niemals mehr laufen können.
Hinweis
Im Extremfall kann der Wartezustand der Threads einer Anwendung zu einem Phänomen führen, das unter der Bezeichnung Deadlock bekannt ist. Dabei befinden sich ausnahmslos alle Threads im blockierten Wartezustand. Die Anwendung hängt sich in diesem Moment auf.
Wir wollen nun die vorgestellten Methoden in einem Beispiel testen. Dazu werden wir ein Programm entwickeln, das in Lage ist, Zahlen zu erzeugen. Das ist eigentlich nichts Weltbewegendes, und wir haben auch schon in den anderen Beispielen Zahlen erzeugt. Das Besondere ist jedoch, dass jede Zahl genau einmal von einem Verbraucher ausgewertet werden soll. Der Verbrauch soll durch eine Konsolenausgabe simuliert werden. Erzeuger und Konsument sollen in einem eigenen Thread laufen.
    // --------------------------------------------------------------
    // Beispiel: ...\ Kapitel 11\Zahlenkonsument
    // --------------------------------------------------------------
    class Program {
      public static bool finished = false;
      public static bool thread1Waiting = false;
      public static bool thread2Waiting = false;
      static void Main(string[] args) {
        MyNumber zahl = new MyNumber();
        ProduceNumber prod = new ProduceNumber(zahl);
        ConsumeNumber cons = new ConsumeNumber(zahl);
        Thread thread1, thread2;
        // Threads instanziieren
        thread1 = new Thread(new ThreadStart(prod.MakeNumber));
        thread2 = new Thread(new ThreadStart(cons.GetNumber));
        // Threads starten
        thread1.Start();
        thread2.Start();
        Console.ReadLine();
      }
    }
    // ------ erzeugt eine Zahl ------------
    class ProduceNumber {
      private MyNumber obj;
      public ProduceNumber(MyNumber obj) {
        this.obj = obj;
      }
      public void MakeNumber() {
        Random rnd = new Random();
        Monitor.Enter(obj);
        for (int i = 0; i <= 10; i++) {
          Program.thread1Waiting = true;
          // falls der Konsumer-Thread noch nicht im Wartezustand ist,
          // selbst in den Wartezustand gehen
          if(Program.thread2Waiting == false)
            Monitor.Wait(obj);
          obj.Number = rnd.Next(0, 1000);
          Console.WriteLine("Nummer {0} erzeugt", obj.Number);
          // dem nächsten in der Warteschlange stehenden Objekt
          // den Monitor übergeben
          Monitor.Pulse(obj);
          Program.thread2Waiting = false;
        }
        Program.finished = true;
        Monitor.Exit(obj);
      }
    }
    // --------- verbraucht eine Zahl -------------
    class ConsumeNumber {
      private MyNumber obj;
      public ConsumeNumber(MyNumber obj) {
        this.obj = obj;
      }
      public void GetNumber() {
        Monitor.Enter(obj);
        // wenn sich der Erzeugerthread im Wartezustand
        // befindet, ihn 'bereit' schalten
        if(Program.thread1Waiting)
          Monitor.Pulse(obj);
        Program.thread2Waiting = true;
        while(Monitor.Wait(obj)) {
          Console.WriteLine("Nummer {0} verbraucht",obj.Number);
          Monitor.Pulse(obj);
          if(Program.finished) Thread.CurrentThread.Abort();
        }
        Monitor.Exit(obj);
      }
    }
    // ------------ repräsentiert eine Zahl -------------------
    class MyNumber {
      private int intValue;
      public int Number {
        get {return intValue;}
        set {intValue = value;}
      }
    }
Der Kern der Anwendung wird durch die beiden Klassen ProduceNumber und ConsumeNumber beschrieben. ProduceNumber erzeugt mit der Methode MakeNumber auf Basis des Zufallszahlengenerators Zahlen zwischen 0 und 999 und schreibt diese in das Feld eines Objekts vom Typ der Klasse MyNumber, das in Main erzeugt wird und dessen Referenz den Konstruktoren der Klassen ConsumeNumber und ProduceNumber übergeben wird. Damit ist sichergestellt, dass sowohl der Erzeuger als auch der Verbraucher mit demselben MyNumber-Objekt operieren. Betrachten wir nun die prinzipielle Arbeitsweise des Verbrauchers und des Konsumenten unter der Prämisse, dass zuerst der Erzeugerthread und danach der Verbraucherthread gestartet wird. Der gesamte Code in der Routine MakeNumber ist synchronisiert. Insgesamt werden elf Zahlen in einer Schleife erzeugt. Direkt nach dem Schleifeneintritt wird die Wait-Methode des Monitors aufgerufen und die Sperre des Objekts vom Typ MyNumber aufgehoben. Jetzt kann ein anderer Thread auf das freigegebene MyNumber-Objekt zugreifen und die Zahl »verbrauchen«, die einen Schleifendurchlauf zuvor erzeugt worden ist. Der Erzeugerthread verharrt so lange in Wartestellung, bis der Verbraucherthread Pulse aufruft und den Erzeugerthread wieder in den Zustand bereit versetzt. Ist dessen Wartezustand aufgehoben, wird die nächste Zahl erzeugt. Bevor der nächste Schleifendurchlauf ausgeführt wird, wird der Verbraucher mit Pulse in den Zustand bereit erhoben. In der Methode GetNumber des Verbrauchers ist der Programmcode ebenfalls synchronisiert. Direkt nach dem Eintritt in den Synchronisierungsabschnitt wird Pulse aufgerufen, um den wartenden Erzeugerthread nach dem Start der Anwendung in den Zustand bereit zu versetzen. Anschließend ruft der Konsument Wait auf. Damit wird der Monitor des Objekts an den Erzeuger weitergegeben, der eine Zahl erzeugt. Gibt der Erzeuger die Sperre an den Konsumenten zurück, wird die neue Zahl zuerst an der Konsole angezeigt und anschließend Pulse aufgerufen. Jetzt ist der Erzeugerthread wieder im Zustand bereit, während der Konsument seinerseits anschließend den Monitor freigibt und sich in den Wartezustand versetzt. Damit der Verbraucherthread überhaupt erfährt, wann der Erzeuger die letzte Zahl bereitgestellt hat, ist die boolesche Variable finished in Class1 deklariert, die vom Erzeuger nach dem letzten Schleifendurchlauf auf true gesetzt wird. So viel zur prinzipiellen Arbeitsweise. Es gibt aber noch ein Problem, dem wir bisher noch keine Beachtung geschenkt haben: Wir können nämlich nicht garantieren, dass der Erzeugerthread als Erstes gestartet wird. Erhält nach dem Starten der Anwendung der Konsument vor dem Erzeuger die CPU, würde es zu einem klassischen Deadlock kommen, wenn sich beide Threads gleichzeitig im Zustand warten befinden. Wir müssen also eine genaue Steuerung des Programmablaufs in der Weise erzwingen, dass sich der Verbraucherthread im Wartezustand befindet, wenn die erste Zahl erzeugt wird. Diese Steuerung wird über die beiden booleschen Variablen thread1Waiting und thread2Waiting erreicht, deren Auswertung garantiert, dass sich – unabhängig von der Startreihenfolge – zu einem gegebenen Zeitpunkt immer nur ein Thread im Wartezustand befindet. Hier klicken, um das Bild zu vergrößern Abbildung 11.5 Die Ausgabe des Projekts »Zahlenkonsument« ### 11.2.9 Das Synchronisationsobjekt »Mutex« Die Klasse Monitor eignet sich nur zur Synchronisation von Threads, die innerhalb eines Prozessraums laufen. Manchmal müssen Abläufe aber auch über Prozessgrenzen hinweg synchronisiert werden. In diesen Fällen müssen Sie die Klasse Mutex einsetzen. Ich möchte Ihnen hierzu ein Beispiel zeigen, bei dem ein Mutex-Objekt dazu benutzt wird, zu verhindern, dass eine Anwendung mehrfach gestartet werden kann.
    // ------------------------------------------------------------
    // Beispiel: ...\Kapitel 11\MutexDemo
    // ------------------------------------------------------------
    class Program {
      private static Mutex mutex;
      static void Main(string[] args) {
        if (IsApplicationStarted()) {
          Console.WriteLine("Die Anwendung wurde bereits gestartet");
          Console.WriteLine("Ein zweiter Start ist nicht möglich.");
        }
        else {
          Console.WriteLine("Die Anwendung wird gestartet.");
          Console.WriteLine("Die Anwendung läuft.");
        }
        Console.ReadLine();
      }
      public static bool IsApplicationStarted() {
        string mutexName = Application.ProductName;
        mutex = new Mutex(false, mutexName);
        if (mutex.WaitOne(0, true))
          return false;
        else
          return true;
      }
    }
Ein Mutex ist ein einfaches Systemobjekt und durch einen eindeutigen Namen gekennzeichnet. Ein Mutex-Objekt gestattet nur jeweils einem Thread exklusiven Zugriff auf die gemeinsam genutzte Ressource. In unserem Beispiel wird das die Anwendung selbst sein. Wenn ein Thread ein Mutex-Objekt erhält, wird ein zweiter Thread, der dieses Objekt abruft, so lange angehalten, bis der erste Thread das Mutex-Objekt freigibt. Die Klasse Mutex stellt mehrere Konstruktoren zur Verfügung. Für unsere Belange ist der geeignet, dem wir den Namen des Mutex mitteilen können. Die Schwierigkeit besteht bei der Namensvergabe darin, dass der Name systemeindeutig sein muss, um den Mutex identifizieren zu können. Es bietet sich hier der Anwendungsname an, obwohl dieser auch keine Garantie für Eindeutigkeit gibt. Möglicherweise müssen Sie hier noch Zusatzinformationen hinzufügen, um die Eindeutigkeit zumindest mit hoher Wahrscheinlichkeit zu gewährleisten. Der entsprechende Konstruktor erwartet darüber hinaus auch noch einen booleschen Wert, der angibt, ob dem aufrufenden Thread der anfängliche Besitz des Mutex zugewiesen werden soll. Er ist true, um dem aufrufenden Thread den anfänglichen Besitz des benannten Mutex zuzuweisen. Über die Methode WaitOne kann eine Ressource das Mutex-Objekt anfordern. Ist der Rückgabewert true, ist das Objekt nicht im Besitz eines anderen Threads, sonst wäre der Rückgabewert false. WaitOne werden zwei Argumente übergeben: Das erste beschreibt ein Zeitintervall, das angibt, wie lange auf ein Signal gewartet werden soll. Über das zweite Argument können Sie bei Anwendungen, die vor dem Warten auf den Mutex den Zugriff auf Objekte oder Klassen über lock sperren, festlegen, dass die Sperrung vor dem Warten aufgehoben und nach dem Warten wieder gesetzt wird. Für uns hat dieser Parameter keine Bedeutung, wir setzen ihn auf true. Im Beispielprogramm dient die Methode IsApplicationStarted dazu, zu prüfen, ob das Mutex-Objekt sich bereits im Besitz eines anderen Threads befindet. Je nachdem, wie das Ergebnis der Prüfung ausfällt, wird eine entsprechende Konsolenausgabe erscheinen. ### 11.2.10 Das Attribut »MethodImpl« Es gibt noch eine weitere Alternative, um die Synchronisierung zwischen mehreren Threads zu erzielen: Verwenden Sie das Attribut MethodImpl. Die zugrunde liegende Klasse ist im Namespace System.Runtime.CompilerServices zu finden. Das Attribut kann nur auf Konstruktoren und Methoden angewendet werden. Es ersetzt die Klasse Monitor mit dem Unterschied, dass nicht nur ein bestimmtes Codesegment eingehüllt wird, sondern in einem Zug gleich die gesamte Methode. Somit kann auch nur immer ein Thread gleichzeitig diese Methode ausführen. Dazu ein Beispiel:
    [MethodImpl(MethodImplOptions.Synchronized)]
    public void Calculate() {
      // Anweisungen
    }
Dem Attribut MethodImpl können verschiedene Parameter übergeben werden. Zur Synchronisation verwenden Sie MethodImplOptions.Synchronized. ### 11.2.11 Grundlagen asynchroner Methodenaufrufe Wird aus einer Methode A heraus die Methode B aufgerufen, wird A erst dann mit den Operationen fortfahren, wenn B vollständig abgearbeitet ist. Die Ausführung der beiden Methoden erfolgt hintereinander, was als synchron bezeichnet wird. Synchrone Operationen haben einen gravierenden Nachteil, denn solange die Methode B ausgeführt wird, ist die Methode A blockiert. Um diese Problematik zu vermeiden, sollten beide Methoden asynchron, d. h. parallel nebeneinander operieren. Asynchrone Bearbeitung setzt mindestens zwei Threads voraus. Sie haben auf den vergangenen Seiten die wichtigsten Techniken kennengelernt, um mit Threads zu arbeiten. Sie wissen nun, wie Sie Threads erzeugen und diese möglicherweise synchronisieren können, damit Elemente nicht in einem ungültigen Zustand hinterlassen werden. Ihnen dürfte dabei nicht entgangen sein, dass die Technik sehr komplex ist und einer genauen Planung bedarf, um keine unbeabsichtigten und bösen Überraschungen zu erleben. Auch in der .NET-Klassenbibliothek finden sich sehr viele Klassen, die Dienste anbieten, deren Ausführung möglicherweise länger dauern kann. Die Dateioperationen zum Lesen und Schreiben zählen dazu. Betrachten wir dazu exemplarisch die Klasse FileStream, die das Schreiben in eine Datei bzw. das Lesen aus einer Datei ermöglicht. (Anmerkung: Die Klassen zur Ein- und Ausgabe werden in Kapitel 29, »LINQ to SQL«, behandelt.) Neben den obligatorischen Methoden Read und Write, die beide synchron ausgeführt werden, werden von dieser Klasse aufgrund der weisen Voraussicht des .NET-Entwicklerteams auch die asynchron operierenden Methoden BeginRead und BeginWrite veröffentlicht. Sehen wir uns kurz die Definition der erstgenannten Methode an, die aus einem Datenstrom in ein byte-Array einliest:
    public override IAsyncResult BeginRead(byte[] array,
                                         int offset,
                                         int numBytes,
                                         AsyncCallback userCallback,
                                         object stateObject);
Der Rückgabewert des Methodenaufrufs ist ein Objekt, das die Schnittstelle IAsyncResult implementiert. Der Parameter vom Typ AsyncCallback ist ein Delegate, das eine Methode im Client beschreibt, die nach der Beendigung der Leseoperation aufgerufen wird. Sowohl BeginRead als auch BeginWrite haben jeweils eine Partnermethode: EndRead und EndWrite.
    public override int EndRead(IAsyncResult asyncResult);
    public override void EndWrite(IAsyncResult asyncResult);
Auch diese Methoden erwarten eine Referenz vom Typ IAsyncResult. Zwei Dinge fallen sofort auf: Die Methoden arbeiten gemäß der Dokumentation asynchron, ohne dass im Aufrufer explizit ein separater Thread gestartet werden muss. Diese Leistung wird von den Methoden intern erbracht. Es treten zwei Typen auf, denen Sie hier zum ersten Mal begegnen und deren Bedeutung noch unbekannt ist: IAsyncResult und AsyncCallback. Wir wollen uns nun mit der Codierung einer asynchronen Ausführung beschäftigen. Danach wird auch die im ersten Moment sehr kompliziert erscheinende Parameterliste asynchron arbeitender Methoden (wie BeginRead) in einem anderen Licht erscheinen. ### 11.2.12 Asynchroner Methodenaufruf Der C#-Compiler stellt mit BeginInvoke und EndInvoke einem Delegate zwei Methoden zur Verfügung, die im Rahmen einer asynchronen Operation von entscheidender Bedeutung sind.
Hinweis
Beachten Sie bitte, dass es sich bei BeginInvoke und EndInvoke um sprachspezifische Methoden handelt, die nicht in der Klasse Delegate definiert sind, aber dennoch von jedem .NET-Compiler veröffentlicht werden sollten – möglicherweise auch unter einem anderen Namen.
Die Methode BeginInvoke ist sehr mächtig, denn wenn Sie sie auf der Referenz eines Delegates aufrufen, wird ein Hintergrundthread erzeugt, in dem die vom Delegate beschriebene Methode ausgeführt wird. Der aufrufende Thread macht mit seiner eigenen Arbeit weiter, anstatt auf die Beendigung der aufgerufenen Methode zu warten. Dazu ein kleines Beispiel. Nehmen wir an, dass die Methode DoSomething aufgerufen werden soll, die eine längere Zeit zur Ausführung benötigt. DoSomething sei wie folgt definiert:
    public void DoSomething() {
      for(int i = 0; i <= 30; i++) {
        Console.Write(".X.");
        Thread.Sleep(10);
      }
    }
Ein Client, der diese Methode asynchron ausführen möchte, kann ein Delegate deklarieren und diesem die Adresse der Methode DoSomething übergeben:
    public delegate void MyDelegate();
    ...
    MyDelegate del = new MyDelegate(obj.DoSomething);
    del.BeginInvoke(...);
Das reicht bereits aus, um DoSomething in einem separaten Thread abzuarbeiten. Dem Aufruf von BeginInvoke müssen Argumente übergeben werden, die unsere Anweisung noch nicht enthält. Sehen wir uns deshalb nun die Definition von BeginInvoke an.
    public IAsyncResult BeginInvoke([Parameterliste ,]
                                    AsyncCallback, Object);
Aufgerufen wird BeginInvoke auf die Instanz eines Delegates, das auf eine bestimmte Methode zeigt. Weist die aufzurufende Methode eine Parameterliste auf, müssen die erforderlichen Argumente von BeginInvoke an die Methode weitergeleitet werden. Dazu dient die optionale Parameterliste. Theoretisch wäre das bereits vollkommen ausreichend, um die aufgerufene Methode asynchron auszuführen. In der Regel benötigt der aufrufende Code aber Kenntnis von der Beendigung der asynchronen Ausführung, beispielsweise wenn er die Rückgabewerte verarbeitet. Folglich muss es eine Möglichkeit geben, die es der asynchron aufgerufenen Methode ermöglicht, den Aufrufer davon zu unterrichten, dass sie ihre Operationen beendet hat. Dabei kann es sich nur um den Aufruf einer Methode im Initiator der asynchronen Operation handeln. Konsequenterweise muss der asynchron aufgerufenen Methode die Adresse der Rückrufmethode im Aufrufer bekannt sein. Das klingt wieder verdächtig nach einem Delegate – und tatsächlich ist dem so, denn dem Aufruf von BeginInvoke werden nicht nur die Argumente übergeben, die die asynchron aufgerufene Methode benötigt, sondern darüber hinaus auch ein Objekt vom Typ AsyncCallback, bei dem es sich um das erforderliche Delegate handelt. Die Definition des Delegates AsyncCallback lautet:
    public delegate void AsyncCallback(IAsyncResult ar);
Die Methode, die aus der asynchron ausgeführten Methode zurückgerufen wird, muss den Rückgabetyp void aufweisen und einen Parameter vom Typ IAsyncResult definieren. BeginInvoke verfügt noch über einen weiteren Parameter vom Typ object. Hier kann beim Start der asynchronen Operation ein beliebiges Objekt übergeben werden, das Informationen beliebiger Art enthält. Das hört sich komplizierter an, als es tatsächlich ist. Daher wollen wir den Ablauf schrittweise an einem kleinen Beispiel verfolgen. Gegeben seien dazu die beiden Klassen Program und Demo wie folgt:
    class Program {
      static void Main(string[] args) {
        ...
      }
    }
    class Demo {
      public void DoSomething() {
        ...
      }
    }
Aus Main heraus soll die Methode DoSomething in der Klasse Demo asynchron aufgerufen werden. Diese Forderung bewirkt, dass wir BeginInvoke auf einem Delegate aufrufen müssen, das die asynchron auszuführende Methode im Objekt vom Typ Demo beschreibt. Dazu wird zunächst ein Delegate mit
    public delegate void MyDelegate();
deklariert. Anschließend verschaffen wir uns ein Objekt vom Typ des Delegates, dem als Argument die asynchron auszuführende Methode übergeben wird.
    private MyDelegate del;
    ...
    del = new MyDelegate(obj.DoSomething);
Mit
    del.BeginInvoke(...);
wird die asynchrone Ausführung von DoSomething in einem Hintergrundthread gestartet. Allerdings ist die Anweisung noch unvollständig – symbolisiert durch die Punkte. Wir sollten in Program nämlich noch eine Methode bereitstellen, mit der der Hintergrundthread das Objekt vom Typ Program über das Ende seiner Operation benachrichtigt. Die Definition der Rückrufmethode muss der Vorgabe des Delegates AsyncCallback entsprechen, demnach also einen Parameter vom Typ IAsyncResult enthalten. Wir nennen diese Methode CallbackMethod.
    class Program {
      ...
      static void Main(string[] args) {...}
      public static void CallbackMethod(IAsyncResult ar) {
        ...
      }
    }
Das Objekt vom Typ IAsyncResult entspricht dem Rückgabewert von BeginInvoke. Es veröffentlicht insgesamt sechs Eigenschaften. Dazu gehört unter anderem auch IsCompleted. Über IsCompleted kann der Aufrufer jederzeit feststellen, ob die asynchrone Ausführung bereits beendet ist. Eine zweite, sehr interessante Eigenschaft ist AsyncState, die genau das Objekt abruft, das als letzter Parameter dem Aufruf von BeginInvoke übergeben worden ist. Sie werden später in einem anderen Beispiel die sinnvolle Auswertung dieses Objekts sehen. Wir wollen nun unser Beispiel komplettieren und Code einsetzen, der tatsächlich einige Zeit in Anspruch nimmt, damit wir den Effekt des asynchronen Aufrufs beobachten können.
    // --------------------------------------------------------------
    // Beispiel: ...\Kapitel l1\AsynchronerAufruf_1
    // --------------------------------------------------------------
    public delegate void MyDelegate();
    class Program {
      private static MyDelegate del;
      static void Main(string[] args) {
        Demo obj = new Demo();
        del = new MyDelegate(obj.DoSomething);
        AsyncCallback callback = new AsyncCallback(CallbackMethod);
        // Methode AsyncTest in Demo asynchron aufrufen
        del.BeginInvoke(callback, null);
        // zeitaufwendige Ausführung
        for(int i = 0; i <= 100; i++) {
          Console.Write(".");
          Thread.Sleep(10);
        }
        Console.ReadLine();
      }
      // zurückgerufene Methode
      public static void CallbackMethod(IAsyncResult ar) {
        Console.Write("Ich habe fertig.");
      }
    }
    class Demo {
      // asynchron aufzurufende Methode
      public void DoSomething() {
        // zeitintensive Ausführung
        for(int i = 0; i <= 30; i++) {
          Console.Write("X");
          Thread.Sleep(10);
        }
      }
    }
In Abbildung 11.6 ist das Ergebnis des Aufrufs zu sehen. Es ist eindeutig zu erkennen, dass die Punkte und »X«-Zeichen mehr oder weniger abwechselnd ausgegeben werden, denn beide Methoden arbeiten parallel. Beendet wird die asynchrone Operation durch den Rückruf von CallbackMethod, was durch die Ausgabe des bekannten Satzes »Ich habe fertig« bestätigt wird. Hier klicken, um das Bild zu vergrößern Abbildung 11.6 Ausgabe eines asynchronen Aufrufs ### 11.2.13 Asynchroner Aufruf mit Rückgabewerten Möglicherweise liefert die asynchrone Methode als Resultat ihrer Operation einen Rückgabewert. Vielleicht werden auch über die Parameterliste Ergebnisse bereitgestellt. Wird aus dem Hintergrundthread heraus die Rückrufmethode des Initiators der asynchronen Operation aufgerufen, stehen die Ergebnisse jedoch nicht automatisch zur Verfügung. Sie müssen ausdrücklich abgerufen werden. Dazu dient die Methode EndInvoke des Delegates.
    public Datentyp EndInvoke ([Parameterliste,] IAsyncResult);
Wie bei BeginInvoke müssen Sie auch an EndInvoke eine vorgeschriebene Parameterliste übergeben, die nicht identisch mit der Parameterliste von BeginInvoke ist: Sie darf nur die Referenzparameter der asynchronen Methode enthalten, damit EndInvoke die Resultate dort hineinschreiben kann. Die Angabe der Werteparameter ist nicht erlaubt. Der einzige grundsätzlich immer zwingend erforderliche Parameter ist vom Typ IAsyncResult. Hier wird das Objekt übergeben, das beim Aufruf von BeginInvoke dem letzten Parameter übergeben worden ist. Wir wollen nun das Beispiel AsynchronerAufruf_1 ändern, um zu sehen, wie eine asynchrone Methode behandelt wird, die sowohl Werte- als auch Referenzparameter erwartet und darüber hinaus auch noch einen Rückgabewert hat. Dazu implementieren wir die Methode DoSomething wie folgt:
    public string DoSomething(int x, ref long y) {
      // zeitaufwendige Ausführung
      for(int i = 0; i <= 30; i++) {
        Console.Write("X");
        Thread.Sleep(10);
      }
      y = 12345;
      return "Ich habe fertig.";
    }
Die Parameterliste enthält jetzt den Referenzparameter y und den Werteparameter x, außerdem liefert die Methode eine Zeichenfolge zurück. Die Änderung der Signatur hat natürlich auch im auslösenden Thread Konsequenzen. Das Delegate, das den Aufruf der Methode kapselt, muss an die veränderten Bedingungen angepasst werden:
    public delegate string MyDelegate(int x, ref long y);
Gleiches gilt auch für den Start der asynchronen Bearbeitung, denn nun reicht es nicht mehr aus, mit BeginInvoke einfach nur ein Delegate auf die Rückrufmethode zu übergeben sowie die Referenz auf ein Objekt, in das der asynchrone Aufruf Informationen schreiben könnte. Wir müssen stattdessen auch die Parameter der asynchronen Methode in der richtigen Reihenfolge bedienen:
    del.BeginInvoke(intVar, ref lngVar, callback, null);
DoSomething nimmt nun eine Kopie des int-Wertes und die Adresse des long-Wertes entgegen, kann mit diesen die erforderlichen Operationen ausführen und zum Abschluss durch Aufruf der über callback bekannt gegebenen Adresse die Methode CallbackMethod informieren. Der Implementierung der Rückrufmethode kommt nun eine entscheidende Bedeutung zu. Es gilt, sowohl den Rückgabewert als auch den in diesem Fall geänderten Inhalt der Variablen lngVar auszuwerten. Dem Aufruf von EndInvoke übergeben wir die Adresse von lngVar und holen uns den Rückgabewert an der Konsole ab:
    public static void MyCallbackProc(IAsyncResult ar) {
      Console.Write(del.EndInvoke(ref lngVar, ar));
      Console.Write("..Wert y = {0}", lngVar);
    }
Die Konsolenausgabe bestätigt, dass unser Unterfangen von Erfolg gekrönt ist: Wir erhalten sowohl die Zeichenfolge als auch den veränderten Inhalt des Feldes intVar. Zum Abschluss fassen wir das Beispielprogramm noch einmal zusammen:
    // --------------------------------------------------------------
    // Beispiel: ...\ Kapitel 11\AsynchronerAufruf_2
    // --------------------------------------------------------------
    public delegate string MyDelegate(int x, ref long y);
    class Program {
      private static MyDelegate del;
      private static int intVar = 4711;
      private static long lngVar;
      static void Main(string[] args) {
        Demo obj = new Demo();
        del = new MyDelegate(obj.DoSomething);
        AsyncCallback callback = new AsyncCallback(CallbackMethod);
        // Methode AsyncTest in Demo asynchron aufrufen
        del.BeginInvoke(intVar, ref lngVar, callback, null);
        // zeitaufwendige Ausführung
        for(int i = 0; i <= 100; i++) {
          Console.Write(".P.");
          Thread.Sleep(10);
        }
        Console.ReadLine();
      }
      public static void CallbackMethod(IAsyncResult ar) {
        Console.Write(del.EndInvoke(ref lngVar, ar));
        Console.Write("..Wert y = {0}", lngVar);
      }
    }
    class Demo {
      public string DoSomething(int x, ref long y) {
        // zeitaufwendige Ausführung
        for(int i = 0; i <= 30; i++) {
          Console.Write(".X.");
          Thread.Sleep(10);
        }
        y = 12345;
        return "Ich habe fertig.";
      }
    }
Hier klicken, um das Bild zu vergrößern Abbildung 11.7 Ausgabe des Beispiels »AsynchronerAufruf_2« ### 11.2.14 Eine Klasse mit asynchronen Methodenaufrufen Am Anfang dieses Abschnitts wurde schon darauf hingewiesen, dass einige Klassen der .NET-Klassenbibliothek Methoden mit asynchroner Verarbeitung anbieten. Die Klasse FileStream im Namespace System.IO ist ein Beispiel dafür. Es werden allerdings nicht die Methoden BeginInvoke und EndInvoke aufgerufen, sondern zwei ähnlich lautende: BeginRead und EndRead bzw. BeginWrite und EndWrite. Wir wollen uns nun ansehen, wie eine Klasse aufgebaut ist, die ähnlich wie FileStream implementiert ist. Dabei lernen wir einerseits, wie wir die asynchronen Methoden der Klassen des .NET Frameworks behandeln müssen, andererseits aber auch, diese Technik in eigenen Klassen zu nutzen. Am Anfang steht die Idee, eine Methode zu entwickeln, von der wir annehmen, dass sie in Abhängigkeit von den Umgebungsbedingungen und der Art der Operation eine längere Zeit zur Bearbeitung in Anspruch nehmen kann. Wir wollen diese Methode nachfolgend Calculate nennen, die Klasse dazu Mathematics.
    class Mathematics {
      public int Calculate(int x) {
        Console.Write("---Bearbeitung startet---");
        for (int i = 0; i <= 20; i++) {
          Console.Write(".X.");
          Thread.Sleep(10);
        }
        Console.Write("---Bearbeitung beendet---");
        return x * x;
     }
    }
Die for-Schleife simuliert eine länger andauernde Operation. Diese Implementierung arbeitet synchron. Da wir uns bewusst sind, dass Calculate vielleicht auch eine Stunde zur vollständigen Ausführung brauchen könnte (wir sind mit unserer Annahme sehr großzügig), bieten wir zusätzlich eine asynchrone Variante an. Dazu benötigen wir zwei weitere Methoden, die einer allgemeinen Konvention folgend als BeginXxx und EndXxx bezeichnet werden – in unserer Klasse demnach BeginCalculate und EndCalculate. Die noch unvollständige Klassenstruktur sieht dann folgendermaßen aus:
    class Mathematics {
      // Die Methode Calculate wird synchron ausgeführt.
      public int Calculate(int x) {
        ...
      }
      // Start der asynchronen Ausführung
      public ... BeginCalculate(...) {
        ...
      }
      // Beenden der asynchronen Ausführung
      public ... EndCalculate(…) {
        ...
      }
    }
An dieser Stelle kommt es zu der wichtigsten Entscheidung überhaupt. Was wir beabsichtigen, ist die asynchrone Ausführung der Methode Calculate. Asynchronität heißt aber auch, dass ein weiterer Thread gestartet werden muss, sobald die Methode BeginCalculate aufgerufen wird. Wenn wir in dieser Methode ein Objekt vom Typ Thread erzeugen und seinem Konstruktor ein Delegate übergeben würden, bräuchten wir auch noch ein Objekt, das das Interface IAsyncResult implementiert, müssten zwangsläufig dessen Methoden implementieren usw. Die Entwicklung auf diese Weise zu gestalten, ist sehr aufwendig. Es gibt eine viel einfachere Lösung, da die beiden Methoden BeginInvoke und EndInvoke genau das leisten, was wir brauchen. Also benutzen wir sie auch, um das Ziel effizient zu erreichen. Dazu wird die Logik, die in den Abschnitten 11.2.12 und 11.2.13 beschrieben wurde, innerhalb der Klasse Mathematics implementiert.
    // --------------------------------------------------------------
    // Beispiel: ...\ Kapitel 11\AsynchronerAufruf_3
    // --------------------------------------------------------------
    public delegate int CalculateHandler(int x);
    class Mathematics {
      CalculateHandler del;
      // Die Methode Calculate wird synchron ausgeführt.
      public int Calculate(int x) {
        Console.Write("---Bearbeitung startet---");
        for (int i = 0; i <= 20; i++) {
          Console.Write("X");
          Thread.Sleep(10);
        }
        Console.Write("---Bearbeitung beendet---");
        return x * x;
      }
      // Start der asynchronen Ausführung
      public IAsyncResult BeginCalculate(int intVar,
                AsyncCallback callback, object state) {
        del = new CalculateHandler(Calculate);
        // Aufruf der Methode Calculate, die in einem eigenen
        // Thread ausgeführt wird
        return del.BeginInvoke(intVar, callback, state);
      }
      // Beenden der asynchronen Ausführung
      public int EndCalculate(IAsyncResult ar) {
        return del.EndInvoke(ar);
      }
    }
Dem Aufruf der Methode BeginCalculate werden die Daten übergeben, die die Methode Calculate für ihre Operation benötigt. In unserem Beispiel handelt es sich nur um einen als Werteparameter deklarierten Integer. Der zweite Parameter erhält die Referenz auf ein Delegate, das die Rückrufmethode im Aufrufer beschreibt. Der dritte und letzte Parameter dient dazu, ein Objekt bereitzustellen, mit dem Daten zwischen dem aufrufenden und dem aufgerufenen Objekt ausgetauscht werden. Ein solches Objekt ist in unserem Beispielcode nicht vorgesehen. Der Aufruf von BeginCalculate orientiert sich an dem von BeginInvoke – und das ist typisch für Klassen im .NET Framework, die asynchrone Methoden offenlegen. Unter ähnlicher Prämisse wird auch EndCalculate implementiert; der Rückgabewert des internen EndInvoke-Aufrufs wird zum Rückgabewert der Instanzmethode. Zum Schluss müssen wir noch testen, ob die Klassenimplementierung auch unseren Anforderungen genügt. Dazu entwickeln wir einen Client mit der Methode Start zum Aufruf der asynchronen Ausführung und einer Methode Results, die als Rückruffunktion vom Server angesteuert wird.
    class Program {
      static void Main(string[] args) {
        Mathematics math = new Mathematics();
        int value = 23;
        AsyncCallback callback = new AsyncCallback(CallbackMethod);
        // Aufruf der asynchronen Ausführung
        math.BeginCalculate(value, callback, math);
        for (int i = 0; i <= 100; i++) {
          Console.Write(".{0}.", i);
          Thread.Sleep(5);
        }
        Console.ReadLine();
      }
      // Diese Methode wird vom Server aufgerufen.
      public static void CallbackMethod(IAsyncResult ar) {
        Mathematics math = (Mathematics)ar.AsyncState;
        // Ergebnis der asynchronen Operation abholen
        int result = math.EndCalculate(ar);
        Console.Write("---Resultat = {0} ", result);
        Console.Write("---FERTIG---");
      }
    }
Beachten Sie bitte die Variable math vom Typ Mathematics. Sie ist als lokale Variable in der Methode Main definiert und daher auch außerhalb der Methode nicht sichtbar. Um dennoch die EndCalculate-Methode dieses Objekts aufrufen zu können, wird die Referenz an den letzten Parameter von BeginCalculate übergeben. In BeginCalculate wird das Objekt an BeginInvoke weitergeleitet. Die Folge ist, dass wir in unserer Callback-Methode nur die Eigenschaft AsyncState auswerten müssen, denn hier kommt das Mathematics-Objekt genau dort an, wo wir dessen EndCalculate-Methode aufrufen müssen, um das Resultat der Operation abzufragen. Die Ausgabe an der Konsole wird so aussehen wie in Abbildung 11.8. Hier klicken, um das Bild zu vergrößern