C# Tipps, Teil 1 - Threads, Prozesse und Synchronisierung

Veröffentlicht: 18. Jan 2006
Von Allen Jones

Auszug aus dem Buch "Microsoft Visual C#.NET - Programmier-Rezepte - Hunderte von Lösungen und Codebeispielen aus der Praxis".

Das nächste Mal, wenn Sie auf ein Problem in Ihrem C#-Code stoßen, das schwierig zu lösen scheint, werfen Sie doch einen Blick in dieses Buch. Hier finden Sie Lösungen und Best Practices zum Schreiben effizienten Codes in C#.

´Microsoft Visual C#.NET Programmierrezepte´ ist Ihre Quelle für hunderte von C#- und .NET Framework-Programmier-Szenarien und -Aufgaben, die mit Hilfe eines konsistenten Problem/Lösungs-Ansatzes erläutert und gelöst werden.

Der logische Aufbau des Buchs erlaubt Ihnen, schnell die Themen zu finden, die Sie interessieren und damit praktische Beispiele, Code-Schnipsel, Best Practices und undokumentierte Vorgehensweisen, die Ihnen helfen, Ihre Arbeit schnell und effizient zu erledigen.

Sämtliche Codebeispiele sind von der Microsoft Press-Website downloadbar.

Microsoft Press


Diesen Artikel können Sie dank freundlicher Unterstützung von Microsoft Press auf MSDN Online lesen. Microsoft Press ist ein Partner von MSDN Online.


Zur Partnerübersichtsseite von MSDN Online

Das Microsoft .NET Framework ist eine extrem anspruchsvolle Plattform, die eine neue Sprache (C#), eine verwaltete Laufzeit (die CLR), eine Plattform für Webanwendungen (Microsoft ASP.NET) und eine umfangreiche Klassenbibliothek zur Erstellung von Anwendungen miteinander kombiniert. Doch so umfassend das .NET Framework auch ist, stellt es nicht alle Funktionen zur Verfügung, die in einem nicht verwalteten Code verfügbar sind. Gegenwärtig enthält das .NET Framework nicht alle Funktionen, die die Win32-API anbietet. Viele Unternehmen verwenden deshalb proprietäre Lösungen, die mit auf COM basierenden Sprachen wie zum Beispiel Microsoft Visual Basic 6 und Microsoft Visual C++ 6 erstellt wurden.

Glücklicherweise verlangt Microsoft nicht von den Unternehmen, die Codebasis aufzugeben, die bereits vorhanden war, als der Wechsel zur .NET-Plattform erfolgte. Das Framework ist stattdessen mit Interoperabilitätsfunktionen ausgestattet, die es Ihnen ermöglichen, von Ihren .NET-Anwendungen aus alten Code zu benutzen und sogar so auf .NET- Assemblies zuzugreifen, als würde es sich dabei um COM-Komponenten handeln. Die Rezepte in diesem Kapitel behandeln die folgenden Themen:

Auf dieser Seite

 Ausführen einer Methode unter Verwendung des Thread-Pools
 Eine Methode asynchron ausführen
 Eine Methode unter Verwendung eines Timers ausführen
 Ausführung einer Methode durch den Zustandswechsel eines WaitHandle-Objektes
 Eine Methode unter Verwendung eines neuen Threads ausführen

Ausführen einer Methode unter Verwendung des Thread-Pools

Aufgabe
Sie möchten eine Methode ausführen und dabei einen Thread aus dem Thread-Pool der .NET-Runtime verwenden.

Lösung
Deklarieren Sie eine Methode mit dem Code, den Sie ausführen möchten; die Methode muss void zurückliefern und Object als Parameter entgegennehmen.

Erstellen Sie eine System.Threading.WaitCallback-Delegateninstanz, die die Methode referenziert. Rufen Sie die statische Methode QueueUserWorkItem der System.Threading.ThreadPool-Klasse auf, und übergeben Sie die Delegateninstanz als Parameter. Die Laufzeit reiht dann die Delegateninstanz ein und führt sie aus, sobald ein Thread-Pool-Thread verfügbar wird.

Beschreibung

Anwendungen, die viele kurzlebige oder eine große Anzahl gleichzeitig auszuführender Threads verwenden, können wegen des Overheads zur Erstellung, Durchführung und der Zerstörung der Threads unter Leistungsmangel leiden. Darüber hinaus ist es nicht unüblich für Multi-Threading-Systeme, dass Threads solange im Leerlauf verweilen, bis sie zur Ausführung kommen. Die Verwendung des Thread-Pools ist eine übliche Lösung, um Skalierbarkeit, Effizienz und Leistung von Multi-Threading-Systemen zu erhöhen.

Das .NET Framework bietet eine einfache Thread-Pool-Implementierung, die Sie durch statische Member der ThreadPool-Klasse erreichen können. Die QueueUserWorkItem-Methode erlaubt Ihnen, eine Methode unter Verwendung eines Thread-Pool-Threads auszuführen, indem sie ein Arbeitselement (Ihre Methode) in einer Thread-Warteschlange einreiht. Dieses Arbeitselement wird durch eine WaitCallback-Delegateninstanz dargestellt, die die Methode referenziert, die Sie ausführen möchten. Sobald ein Thread des Thread-Pools verfügbar wird, kümmert es sich um das nächste Arbeitselement in der Warteschlange und führt es aus. Der Thread führt die Arbeit aus, die ihm zugeteilt wurde, und wenn er beendet ist, terminiert er nicht, sondern kehrt zum Thread-Pool zurück und kümmert sich um das nächste Arbeitselement in der Warteschlange.

Die Verwendung des Thread-Pools der Runtime erleichtert damit die Multi-Threading-Programmierung ungemein; allerdings sollten Sie sich bewusst sein, dass die Implementierung des Thread-Pools sehr einfach und allgemein gehalten ist. Bevor Sie sich für die seine Verwendung entscheiden, sollten Sie die folgenden Punkte bedenken:

  • Der Runtime-Host bestimmt die maximale Thread-Anzahl, die für den Thread-Pool reserviert wird; Sie können die Anzahl nicht über Konfigurationsparameter oder aus verwaltetem Code heraus ändern. Die Standardgrenze ist auf 25 Threads pro Prozessor festgelegt. Diese Anzahl limitiert allerdings nicht die Anzahl von Arbeitselementen, die in der Warteschlange des Pools auf ihre Ausführung warten.

  • Genau so, wie die Runtime es Ihnen erlaubt, den Thread-Pool zu verwenden, um Code direkt auszuführen, verwendet sie ihn auch für viele eigene interne Zwecke. Dazu gehört die asynchrone Methodenausführung (Abschnitt: "Eine Methode asynchron ausführen") und die Ausführung von Timer-Ereignissen (Abschnitt: "Eine Methode unter Verwendung eines Timers ausführen"). Das kann zu einer sehr regen Frequentierung des Thread-Pools führen, und das bedeutet wiederum, dass die Warteschlange recht lang werden kann. Obwohl die Warteschlangenlänge nur durch den zur Verfügung stehenden Arbeitsspeicher begrenzt wird, kann eine sehr lange Warteschlangen zu langen Verzögerungen bei der Thread-Ausführung führen.

  • Sie sollten den Thread-Pool nicht verwenden, um lang andauernde Prozesse auszuführen. Die begrenzte Anzahl an Threads im Thread-Pool führt dazu, dass ein paar sehr lang laufende Threads die Leistungsfähigkeit des gesamten Pools stark beeinträchtigen können. Vor allen Dingen sollten Sie davon absehen, einen Thread-Pool-Thread vorübergehend »einzufrieren«.

  • Sie haben keine Kontrolle über das Inkrafttreten eines Thread-Pool-Threads, und Sie können auch nicht bestimmte Arbeitselemente priorisieren. Die Arbeitselemente werden in der Reihenfolge ausgeführt, in der Sie sie in die Warteschlange eingereiht haben.

  • Wenn ein Arbeitselement eingereiht wurde, kann sein korrelierender Thread nicht abgebrochen oder gestoppt werden.

Das folgende Beispiel demonstriert die Verwendung der ThreadPool-Klasse, um eine Methode namens DisplayMessage auszuführen. Dieses Beispiel übergibt dem Thread-Pool DisplayMessage zweimal - beim ersten Mal ohne Parameter, beim zweiten Mal mit einem MessageInfo-Objekt als Parameter, das Ihnen die anzuzeigende Nachricht zu steuern erlaubt.

using System;
using System.Threading;

// Eine Klasse, die verwendet wird, um Daten an DisplayMessage zu übergeben,
// wenn sie durch den Thread-Pool ausgeführt wird.
public class MessageInfo {

    private int iterations;
    private string message;

    // Konstruktor, der die Konfigurationseinstellungen für den Thread übernimmt.
    public MessageInfo(int iterations, string message) {

        this.iterations = iterations;
        this.message = message;
    }

    // Eigenschaften, um die Konfigurationseinstellungen zu ermitteln.
    public int Iterations { get { return iterations; } }
    public string Message { get { return message; } }
}
 
public class ThreadPoolExample {

    // Eine Nachricht im Konsolenfenster ausgeben.
    public static void DisplayMessage(object state) {

        // Statusparamter in ein MessageInfo-Objekt casten.
        MessageInfo config = state as MessageInfo;

        // Falls der Konfigurationsparameter null ist, wurden der 
        // ThreadPool.QueueUserWorkItem-Methode keine Parameter übergeben;
        // dann die Standardwerte verwenden.
        if (config == null) {
            // Eine vordefinierte Nachricht dreimal im Konsolenfenster ausgeben.
            for (int count = 0; count < 3; count++) {
                Console.WriteLine("Ein Thread-Pool-Beispiel.");
                // Sleep nur für Demonstrationszwecke. Vermeiden Sie das Aussetzen des Threads
                // mit Sleep in echten Anwendungen!
                Thread.Sleep(1000);
            }

        } else {

            // Die angegebene Nachricht entsprechend oft ausgeben.
            for (int count = 0; count < config.Iterations; count++) {

                Console.WriteLine(config.Message);

                // Sleep nur für Demonstrationszwecke. Vermeiden Sie das Aussetzen des Threads
                // mit Sleep in echten Anwendungen!
                Thread.Sleep(1000);
            }
        }
    }

    public static void Main() {

        // Erstellen einer Delegaten-Instanz, um zu erlauben, die DisplayMessage-Methode
        // dem Thread-Pool zwecks Ausführung hinzuzufügen.
        WaitCallback workMethod = 
            new WaitCallback(ThreadPoolExample.DisplayMessage);
        // Ausführen von DisplayMessage unter Verwendung des Thread-Pools und ohne Paramerter.
        ThreadPool.QueueUserWorkItem(workMethod);

        // DisplayMessage unter Verwendung des Thread-Pools und mit der Möglichkeit
        // zur Übergabe eines MessageInfo-Objektes, das DisplayMessage übergeben wird, ausführen.
        MessageInfo info = 
            new MessageInfo(5, "Ein Thread-Pool-Beispiel mit Parametern.");

        ThreadPool.QueueUserWorkItem(workMethod, info);

        // Auf weitere Ausführung warten.
        Console.WriteLine("Hauptmethode beendet. Drücken Sie Return zum Beenden.");
        Console.ReadLine();
    }
}

Eine Methode asynchron ausführen

Aufgabe
Sie müssen die Ausführung einer Methode in einem eigenen Thread beginnen und aufrechterhalten, während andere Methoden gleichzeitig auf anderen Threads laufen. Nachdem die Methode beendet wurde, benötigen Sie allerdings einen Rückgabewert, den die Methode ergab.

Lösung
Deklarieren Sie einen Delegaten mit der gleichen Signatur wie die Methode, die Sie ausführen wollen. Erstellen Sie eine Instanz des Delegaten, der auf die Methode verweist. Rufen Sie die BeginInvoke-Methode der Delegateninstanz auf, um die Ausführung Ihrer Methode zu beginnen. Verwenden Sie EndInvoke, um den Methodenstatus sowie den Rückgabewert der Methode zu bestimmen - Letzteres für den Fall, dass die Methode beendet wurde.

Beschreibung

Typischerweise rufen Sie eine Methode synchron auf - was bedeutet, dass der aufgerufene Code die Programmausführung solange blockiert, bis er komplett abgearbeitet wurde. Für die meisten Zwecke ist das auch gewünscht, da Ihr Code eine bestimmte Aufgabe komplett abschließen soll, bevor er mit weiteren Aufgaben fortfahren kann. Dennoch ist es manchmal sinnvoll, eine Methode asynchron auszuführen, was bedeutet, dass sie in einem separaten Thread gestartet und parallel zu anderen Operationen ausgeführt wird.

Das .NET Framework implementiert ein Schema zur asynchronen Ausführung von Methoden, das es Ihnen erlaubt, jede beliebige Methode unter Verwendung eines Delegaten asynchron laufen zu lassen. Wenn Sie einen Delegaten deklarieren und anschließend kompilieren, generiert der Compiler automatisch zwei Methoden, die die asynchrone Ausführung ermöglichen: BeginInvoke und EndInvoke. Wenn Sie BeginInvoke einer Delegateninstanz aufrufen, wird die Methode, die durch den Delegaten referenziert wird, zur asynchronen Ausführung eingereiht. Die Programmkontrolle wird unmittelbar an die aufrufende Instanz zurückgegeben, und die referenzierte Methode wird im ersten verfügbaren Thread-Pool-Thread ausgeführt.

Die Signatur von BeginInvoke beinhaltet die gleichen Parameter wie die durch die Delegatensignatur festgelegte und übernimmt darüber hinaus zwei weitere für die asynchrone Methodenendebenachrichtigung. Diese zusätzlichen Parameter sind:

  • Eine System.AsyncCallback-Delegateninstanz, die auf eine Methode verweist, die von der Runtime aufgerufen wird, wenn die asynchrone Methode abgeschlossen wurde. Diese Rückruf-Methode wird im Kontext des Thread-Pool-Threads ausgeführt. Die Übergabe von null an die Methode bedeutet, dass keine Methode nach Methodenabschluss aufgerufen werden soll und sie ein anderes Verfahren zum Feststellen des Methodenabschlusses gewählt haben (wird später in diesem Rezept besprochen).

  • Eine Objekt-Referenz, die die Runtime der asynchronen Operation zuordnet. Die asynchrone Methode verwendet dieses Objekt nicht selbst - sie hat noch nicht einmal Zugriff darauf. Aber ein solches Objekt erlaubt Ihrem Code, Zugriff darauf zu nehmen, wenn die Operation abgeschlossen wurde und gestattet es Ihnen, brauchbare Statusinformationen mit der asynchronen Methodenausführung zu verbinden. So erlaubt Ihnen das Objekt beispielsweise, Ergebnisse eingeleiteter Operationen in Situationen zu protokollieren, bei denen Sie mehrere asynchrone Operationen starten, die alle eine gemeinsame Rückrufroutine verwenden, um über den Abschluss der jeweiligen Methode zu informieren.

Die EndInvoke-Methode erlaubt es Ihnen, einen Rückgabewert einer asynchron durchgeführten Methode zu ermitteln, jedoch müssen Sie zuvor feststellen, ob die Methode bereits beendet wurde. Im Folgenden finden Sie vier Vorgehensweisen beschrieben, mit deren Hilfe Sie bestimmen können, ob eine asynchrone Methode bereits beendet wurde oder nicht.

  • Blocken: Blocken stoppt die Ausführung des aktuellen Threads bis die asynchrone Methode ihre Ausführung beendet hat. Daraus folgt, dass die Methode wie eine synchron ausgeführte Methode behandelt wird. Im Gegensatz zu synchron ausgeführten Methoden haben Sie jedoch die Flexibilität, zu entscheiden, wann genau Ihr Code in den geblockten Zustand wechselt, und das gibt Ihnen die Möglichkeit, vor dem Blocken einige vorbereitende Maßnahmen zu ergreifen - falls das erforderlich sein sollte.

  • Polling (Abfragen): Polling macht ein beständiges Zustandstesten einer asynchron ausgeführten Methode notwendig, um herauszufinden, ob diese bereits beendet ist. Dabei handelt es sich um eine ausgesprochen einfache Methode, die aus Verarbeitungssicht nicht sonderlich effizient ist. Sie sollten dabei Schleifen vermeiden, die wertvolle Prozessorzeit verschwenden; am besten ist es, den abfragenden Thread stattdessen mit Sleep auszusetzen. Da das Polling eine Schleife erforderlich macht, sind die Aktionen des wartenden Threads zwar beschränkt - es reicht aber auf jeden Fall, um beispielsweise eine Fortschrittsanzeige zu aktualisieren.

  • Warten: Die Technik des Wartens verwendet ein aus der System.Threading.WaitHandle-Klasse abgeleitetes Objekt, um zu signalisieren, dass eine asynchrone ausgeführte Methode beendet wurde. Das Warten ist eine effizientere Methode als die des Pollings, und darüber hinaus erlaubt sie es Ihnen sogar, auf das Beenden mehrere asynchrone ausgeführter Routinen zu warten. Sie können auch Time-Out-Werte bestimmen, die einen Zeitraum angeben, nach dem der Warte-Thread abgebrochen wird, falls das Warten auf das Beenden der asynchronen Operationen zu lange dauern würde, oder wenn Sie während des Wartens eine Statusanzeige regelmäßig aktualisieren möchten.

  • CallBacks: Ein Callback ist eine Methode, die die Runtime aufruft, wenn eine asynchrone Operation beendet wurde. Der aufrufende Code muss dabei keine Schritte unternehmen, um zu bestimmen, ob die asynchron laufende Methode noch arbeitet oder nicht und kann sich ganz seiner eigenen Arbeit widmen. Callbacks bieten daher die größte Flexibilität, sind aber auch am komplexesten zu behandeln, gerade wenn mehrere asynchrone Methoden die gleiche Callback-Routine zur Methodenendebenachrichtigung verwenden sollen. In diesen Fällen sollten Sie geeignete Zustandsobjekte zur Identifizierung der jeweils beendeten Methode verwenden.

Die AsyncExecutionExample-Klasse des Beispielcodes dieses Rezeptes demonstriert die Verwendung des Schemas zur asynchronen Methodenausführung. Es verwendet einen Delegaten namens AsyncExampleDelegate, um eine Methode namens LongRunningMethod asynchron auszuführen. LongRunningMethode simuliert eine lang andauernde Methode mit einer einstellbaren Dauer (durch Thread.Sleep gesteuert).

Im Folgenden finden Sie den Code für AsyncExampleDelegate und LongRunningMethod:

// Ein Delegat, der Ihnen die asynchrone Ausführung von
// AsyncExecutionExample.LongRunningMethod erlaubt.
public delegate DateTime AsyncExampleDelegate(int delay, string name);

    // Eine eine lange Laufzeit simulierende Methode.
    public static DateTime LongRunningMethod(int delay, string name) {

        Console.WriteLine("{0} : {1} Beispiel - Thread wird gestartet.", 
            DateTime.Now.ToString("HH:mm:ss.ffff"), name);

        // Zeitintensiven Prozess simulieren.
        Thread.Sleep(delay);

        Console.WriteLine("{0} : {1} Beispiel - Thread wird beendet.", 
            DateTime.Now.ToString("HH:mm:ss.ffff"), name);

        // Den Beendigungszeitpunkt zurückliefern.
        return DateTime.Now;
    }

AsyncExecutionExample enthält fünf Beispiele, die verschiedene Ansätze für das Behandeln des Abschlusses von asynchron aufgerufenen Methoden demonstrieren. Eine Beschreibung dieser Methoden und deren zugehöriger Code finden Sie im Folgenden:

Die BlockingExample-Methode führt LongRunningMethode asynchron aus und führt die weitere Ausführung eingeschränkt fort. Nachdem die Verarbeitung abgeschlossen ist, blockiert BlockingExample die weitere Ausführung solange bis LongRunningMethod abgeschlossen ist. Um das Blocken durchzuführen, ruft BlockingExmaple die EndInvoke-Methode der AsyncExampleDelegate-Delegateninstanz auf. Falls LongRunningMethod bereits beendet wurde, kehrt EndInvoke unmittelbar zurück; anderenfalls blockiert BlockingExample die weitere Ausführung solange bis LongRunningMethod abgeschlossen wurde.

// Diese Methode führt LongRunningMethod asynchron aus und fährt mit weiteren
    // Verarbeitungen fort. Sobald die Verarbeitung abgeschlossen ist, blockiert die
    // Methode weitere Verarbeitungen solange bis LongRunningMethod beendet ist.
    public static void BlockingExample() {

        Console.WriteLine(Environment.NewLine + 
            "*** Blocken-Beispiel ausführen ***");

        // LongRunningMethod asynchron ausführen. Null sowohl für den
        // Callback-Delegaten und das asynchrone Zustandsobjekt übergeben.
        AsyncExampleDelegate longRunningMethod = 
            new AsyncExampleDelegate(LongRunningMethod);

        IAsyncResult asyncResult = longRunningMethod.BeginInvoke(2000, 
            "Blocken", null, null);

        // Mit anderen Dingen fortfahren, bis zum Blocken bereit.
        for (int count = 0; count < 3; count++) {
            Console.WriteLine("{0} : Ausführung fortfahren bis zum Blocken bereit " 
                , DateTime.Now.ToString("HH:mm:ss.ffff"));
            Thread.Sleep(200);
        }
        // Blocken, bis die asynchrone Methode beendet wurde und die
        // Rückgabedaten ermitteln.
        Console.WriteLine("{0} : Blocken bis zum Methodenende...",
            DateTime.Now.ToString("HH:mm:ss.ffff"));
        DateTime completion = longRunningMethod.EndInvoke(asyncResult);

        // Ergebniswerte darstellen.
        Console.WriteLine("{0} : Blocken-Beispiel abgeschlossen.",
            completion.ToString("HH:mm:ss.ffff"));
    }

Die PollingExample-Methode führt LongRunningMethod asynchron aus und betritt anschließend eine Abfrage-Schleife, die solange durchlaufen wird, bis LongRunningMethod abgeschlossen ist. PollingExample testet die IsCompleted-Eigenschaft der IAsyncResult-Instanz, die durch BeginInvoke zurückgegeben wird, um zu bestimmen, ob LongRunningMethod bereits abgeschlossen wurde. Anderenfalls ruft PollingExample die ThreadSleep-Methode auf, um weiter zu warten und die Schleife fortzufahren.

// Diese Methode führt LongRunningMethod asynchron aus und dann
    // betritt sie eine Abfrage-(Polling-)Schleife, bis LongRunningMethod
    // abgeschlossen wurde.
    public static void PollingExample() {

        Console.WriteLine(Environment.NewLine + 
            "*** Abfrage-Beispiel ausführen ***");

         // LongRunningMethod asynchron ausführen. Null sowohl für den
         // Callback-Delegaten und das asynchrone Zustandsobjekt übergeben.
         AsyncExampleDelegate longRunningMethod = 
            new AsyncExampleDelegate(LongRunningMethod);

        IAsyncResult asyncResult = longRunningMethod.BeginInvoke(2000, 
            "Abfragen", null, null);

        // Abfragen der asynchronen Methoden, um auf ihre Fertigstellung zu testen. 
        // Falls noch nicht beendet, Thread mit Sleep für 300 ms vor dem nächsten Test aussetzen.
        Console.WriteLine("{0} : Wiederholtes Abfragen (Polling), bis Methode " +
            "beendet wurde...", DateTime.Now.ToString("HH:mm:ss.ffff"));
        while(!asyncResult.IsCompleted) {
            Console.WriteLine("{0} : Abfragen (Polling)...", 
                DateTime.Now.ToString("HH:mm:ss.ffff"));
            Thread.Sleep(300);
        }

        // Rückgabedaten für die asynchrone Methode ermitteln.
        DateTime completion = longRunningMethod.EndInvoke(asyncResult);

        // Rückgabedaten darstellen.
        Console.WriteLine("{0} : Abfrage-(Polling-) Beispiel abgeschlossen.",
            completion.ToString("HH:mm:ss.ffff"));
    }

Die WaitingExample-Methode führt LongRunningMethod asynchron aus und wartet anschließend, bis LongRunningMethod abgeschlossen wurde. WaitingExample verwendet die AsyncWaitHandle-Eigenschaft der IAsyncResult-Instanz, die durch BeginInvoke zurückgegeben wird, um ein WaitHandle zu erhalten und ruft anschließend deren WaitOne-Methode auf. Die Verwendung eines Timeout erlaubt der Methode, den Wartenprozess temporär zu verlassen, damit entweder die Benutzeroberfläche aktualisiert oder der gesamte Prozess wegen zu langen Wartens komplett abgebrochen werden kann.

public static void WaitingExample() {

        Console.WriteLine(Environment.NewLine + 
            "*** Ausführen des Warten-Beispiels ***");

// LongRunningMethod asynchron ausführen. Null sowohl für den
// Callback-Delegaten und das asynchrone Zustandsobjekt übergeben.
AsyncExampleDelegate longRunningMethod = 
            new AsyncExampleDelegate(LongRunningMethod);

        IAsyncResult asyncResult = longRunningMethod.BeginInvoke(2000, 
            "Waiting", null, null);

        // Auf die Fertigstellung der asynchronen Methode warten.
// Timeout erfolgt nach 300 ms; dann die Status im Konsolenfenster
// ausgeben und anschließend wieder warten.
        Console.WriteLine("{0} : Warten bis zum Methodenende...", 
            DateTime.Now.ToString("HH:mm:ss.ffff"));
        while(!asyncResult.AsyncWaitHandle.WaitOne(300, false)) {
            Console.WriteLine("{0} : Warten-Timeout...",
                DateTime.Now.ToString("HH:mm:ss.ffff"));
        }

        // Rückgabedaten der asynchronen Methode ermitteln.
        DateTime completion = longRunningMethod.EndInvoke(asyncResult);

        // Rückgabedaten darstellen.
        Console.WriteLine("{0} : Warten-Beispiel abgeschlossen.",
            completion.ToString("HH:mm:ss.ffff"));
    }

Die WaitAllExample-Methode führt LongRunningMethod asynchron mehrere Male aus und verwendet anschließend ein Array aus WaitHandle-Objekten, um anschließend effizient (ohne großen Prozessorleistungsverlust) auf den Abschluss aller Methoden zu warten.

public static void WaitAllExample() 
    {

        Console.WriteLine(Environment.NewLine + 
            "*** Ausführen des WaitAll-Beispiels ***");

        // Eine ArrayList, die die IAsyncResult-Instanzen für jede gestartete
        // asynchrone Methode speichert.
        ArrayList asyncResults = new ArrayList(3);

        // Drei LongRunningMethods asynchron laufen lassen.
        // Null sowohl für den Callback-Delegaten und das asynchrone Zustandsobjekt übergeben.
        // Die IAsyncResult-Instanz für jede Methode zur ArrayList hinzufügen.
        AsyncExampleDelegate longRunningMethod = 
            new AsyncExampleDelegate(LongRunningMethod);

 
        asyncResults.Add(longRunningMethod.BeginInvoke(3000, 
            "WaitAll 1", null, null));

        asyncResults.Add(longRunningMethod.BeginInvoke(2500, 
            "WaitAll 2", null, null));

        asyncResults.Add(longRunningMethod.BeginInvoke(1500, 
            "WaitAll 3", null, null));

        // Ein Array aus WaitHandle-Objekten erstellen, die zum Warten auf
        // die Fertigstellung der asynchronen Methoden verwendet werden.
        WaitHandle[] waitHandles = new WaitHandle[3];

        for (int count = 0; count < 3; count++) {

            waitHandles[count] = 
                ((IAsyncResult)asyncResults[count]).AsyncWaitHandle;
        }

        // Auf die Fertigstellung aller drei asynchronen Methoden warten.
        // Timeout erfolgt nach 300 ms; dann die Status im Konsolenfenster
        // ausgeben und anschließend wieder warten.
        Console.WriteLine("{0} : Warten bis alle 3 Methoden " + 
            "abgeschlossen sind...", DateTime.Now.ToString("HH:mm:ss.ffff"));
        while(!WaitHandle.WaitAll(waitHandles, 300, false)) {
            Console.WriteLine("{0} : WaitAll Timeout...", 
                DateTime.Now.ToString("HH:mm:ss.ffff"));
        }

        // Die Rückgabedaten jeder Methode untersuchen und feststellen,
        // wann jede Methode beendet wurde.
        DateTime completion = DateTime.MinValue;
        foreach (IAsyncResult result in asyncResults) {

            DateTime time = longRunningMethod.EndInvoke(result);
            if ( time > completion) completion = time;
        }

        // Rückgabewerte darstellen.
        Console.WriteLine("{0} : WaitAll-Beispiel abgeschlossen.",
            completion.ToString("HH:mm:ss.ffff"));
    }

Die Methode CallbackExample führt LongRunningMethod asynchron aus und übergibt eine AsyncCallback-Delegateninstanz (die die CallbackHandler-Methode referenziert). Die referenzierte CallbackHandler-Methode wird automatisch aufgerufen, wenn die asynchrone Methode fertig gestellt wurde, was dazu führt, dass diese Methode ohne Behinderung weiter ausgeführt werden kann.

public static void CallbackExample() {

        Console.WriteLine(Environment.NewLine + 
            "*** Ausführen des Callback-Beispiels ***");

        // LongRunningMethod asynchron ausführen. Eine AsyncCallback-Delegateninstanz
        // übergeben, die die CallbackHandler-Methode referenziert, die wiederum
        // automatisch aufgerufen wird, wenn die asynchrone Methode fertiggestellt wurde.
        // Eine Referenz auf die AsyncExampleDelegate-Delegateninstanz als asynchrones
        // Zustandsobjekt übergeben; anderenfalls hat die Callback-Methode keinen
        // Zugriff auf die Delegateninstanz, dessen EndInvoke-Methode sie aufrufen muss.
        AsyncExampleDelegate longRunningMethod = 
            new AsyncExampleDelegate(LongRunningMethod);

        IAsyncResult asyncResult = longRunningMethod.BeginInvoke(2000, 
            "Callback", new AsyncCallback(CallbackHandler), 
            longRunningMethod);

        // Mit anderen Arbeiten fortfahren.
        for (int count = 0; count < 15; count++) {
            Console.WriteLine("{0} : Mit weiterer Verarbeitung fortfahren...", 
                DateTime.Now.ToString("HH:mm:ss.ffff"));
            Thread.Sleep(200);
        }
    }

    // Eine Methode um den asynchronen Methodenabschluss durch Callbacks zu behandeln.
    public static void CallbackHandler(IAsyncResult result) {

        // Den Verweis zur AsyncExampleDelegate-Instanz aus der IAsyncResult-Instanz
        // extrahieren. Das erlaubt uns, die Rückgabedaten zu ermitteln.
        AsyncExampleDelegate longRunningMethod = 
            (AsyncExampleDelegate)result.AsyncState;

        // Rpckgabedaten der asynchronen Methode ermitteln.
        DateTime completion = longRunningMethod.EndInvoke(result);

        // Rückgabedaten darstellen.
        Console.WriteLine("{0} : Callback-Beispiel abgeschlossen.",
            completion.ToString("HH:mm:ss.ffff"));
    }

Eine Methode unter Verwendung eines Timers ausführen

Aufgabe
Sie müssen eine Methode in einem separaten Thread entweder periodisch oder zu einem bestimmten Zeitpunkt ausführen.

Lösung
Deklarieren Sie eine Methode, die void zurückliefert und ein einzelnes Object als Parameter übernimmt. Erstellen Sie eine System.Threading.TimerCallback-Delegateninstanz, die auf die Methode verweist. Erstellen Sie ein System.Threading.Timer-Objekt, und übergeben Sie es der TimerCallback-Delegateninstanz zusammen mit einem Zustands-Object, das der Timer Ihrer Methode übergibt, wenn der Timer abläuft. Die Runtime wartet anschließend, bis der Timer abgelaufen ist und ruft dann Ihre Methode unter Verwendung eines Thread-Pool-Threads auf.

Beschreibung

Es ist oftmals von Vorteil, eine Methode auf Grund eines bestimmten Zeitplans oder in regelmäßigen Intervallen aufrufen zu können. So könnten Sie beispielsweise automatische Datensicherungen jede Nacht um eins durchführen oder einen Datenzwischenspeicher alle 20 Minuten löschen wollen. Die Timer-Klasse macht eine zeitgesteuerte Ausführung von Methoden recht einfach, indem sie die Ausführung einer Methode zu bestimmten Zeiten erlaubt, die durch einen TimerCallback-Delegaten referenziert wird. Die referenzierte Methode wird anschließend im Kontext eines Threads des Thread-Pools ausgeführt.

Wenn Sie ein Timer-Objekt erstellen, bestimmten Sie dabei zwei Zeitintervalle. Der erste Wert bestimmt eine Verzögerung (in Millisekunden anzugeben) nachdem Ihre Methode ausgeführt werden soll. Geben Sie den Wert 0 an, wenn Sie wünschen, dass die Methode ohne Verzögerung ausgeführt werden soll, und den Wert System.Threading.Timeout.Infinite, um den Timer im ungestarteten Zustand zu erstellen. Der zweite zu übergebende Parameter bestimmt ein Intervall, nach dessen Ablauf das Timer-Objekt Ihre Methode wiederholt aufruft. Falls Sie dabei als Wert 0 oder den Wert Timeout.Infinite angeben, ruft der Timer die Methode nur ein einziges Mal auf (jedenfalls solange der erste Parameter nicht den Wert Timeout.Infinite hatte). Sie können den Wert für das Timer-Intervall als int, long, uint oder System.TimeSpan übergeben.

Sobald Sie das Timer-Objekt erstellt haben, können Sie die Intervalle, die vom Timer verwendet werden, durch die Change-Methode verändern, aber Sie können keine aufzurufende Methode im Nachhinein ändern. Wenn Sie einen Timer schließlich nicht mehr benötigen, sollten Sie seine Dispose-Methode aufrufen, damit die durch ihn belegten System-Ressourcen wieder freigegeben werden können. Die Anwendung von Dispose für ein Timer-Objekt hebt alle geplanten Steuerungen auf.

Die TimerExample-Klasse des folgenden Beispiels demonstriert die Verwendung eines Timer-Objektes, um eine Methode namens TimerHandler auszuführen. Anfänglich wird der Timer so konfiguriert, dass TimerHandler das erste Mal nach zwei Sekunden und anschließend im Ein-Sekunden-Abstand aufgerufen wird. Das Beispiel erlaubt es Ihnen, ein neues Intervall aus der Konsole heraus in Millisekunden anzugeben, was anschließend durch die TimerChanged-Methode zugewiesen wird.

using System;
using System.Threading;

public class TimerExample {

    // Die Methode, die ausgeführt wird, sobald der Timer abgelaufen ist.
    // Zeigt eine Nachricht im Konsolenfenster an.
    private static void TimerHandler(object state) {

        Console.WriteLine("{0} : {1}",
            DateTime.Now.ToString("HH:mm:ss.ffff"), state);
    }

    public static void Main() {

        // Erstellt eine neue TimeCallback-Delegateninstanz, die
        // die statische TimerHandler-Methode referenziert.
        // TimerHandler wird aufgerufen, wenn der Timer abläuft.
        TimerCallback handler = new TimerCallback(TimerHandler);

        // Erstellt ein Zustandsobjekt, das der TimerHandler-Methode
        // übergeben wird, wenn sie ausgelöst wird. In diesem Fall eine
        // darzustellende Nachricht.
        string state = "Timer abgelaufen.";

        Console.WriteLine("{0} : Erstelle Timer.",
            DateTime.Now.ToString("HH:mm:ss.ffff"));

        // Erstellen eines Timers der das erste Mal nach zwei Sekunden und dann
        // jede Sekunde auslöst.
        using (Timer timer = new Timer(handler, state, 2000, 1000)) {

            int period;

            // Das neue Timer-Intervall aus dem Konsolenfenster
            // lesen, solange bis der Anwender den Wert 0 eingegeben hat.
            // Ungültige Werte bedeuten das gleiche wie 0 und beenden das Beispiel.
            do {

                try {
                    period = Int32.Parse(Console.ReadLine());
                } catch {
                    period = 0;
                }

                // Den Timer so umprogrammieren, dass der neue Intervallwert berücksichtigt wird;
                // Das neue Intervall beginnt unmittelbar.
                if (period > 0) timer.Change(0, period);

            } while (period > 0);
        }

        // Wait to continue.
        Console.WriteLine("Hauptmethode abgeschlossen. Drücken Sie Return.");
        Console.ReadLine();
    }
}

Obwohl der Timer in erster Linie für den Aufruf von Methoden in regelmäßigen Abständen verwendet wird, gibt er Ihnen ebenfalls die Möglichkeit, eine Methode zu einem bestimmten Zeitpunkt aufzurufen. Dazu müssen Sie die Differenz zwischen dem aktuellen Zeitpunkt und dem Zeitpunkt ausrechnen, zu dem Sie die Methode aufrufen wollen, wie in der folgenden RunAt-Methode gezeigt. (Die RunAt-Methode ist ein Member der RunAtExample-Klasse des Beispielcodes dieses Rezeptes.)

using System;
using System.Threading;

public class RunAtExample {

    // Die Methode, die ausgeführt wird, wenn der Timer abläuft. Zeigt eine
    // Nachricht im Konsolenfenster an.
    private static void TimerHandler(object state) {

        Console.WriteLine("{0} : Timer abgelaufen",
            DateTime.Now.ToString("HH:mm:ss.ffff"));
    }

    public static void RunAt(DateTime execTime) {

        // Errechnen des Unterschieds zwischen dem gewünschten Ausführungszeitpunktes
        // und der aktuellen Uhrzeit.
        TimeSpan waitTime = execTime - DateTime.Now;

        if (waitTime < new TimeSpan(0)) waitTime = new TimeSpan(0);

        // Erstellen einer neuen TimerCallback-Delegateninstanz, 
        // die eine statische TimerHandler-Methode referentiert.
        // TimerHandler wird aufgerufen, wenn der Timer abläuft.
        TimerCallback handler = new TimerCallback(TimerHandler);

        // Einen Timer erstellen, der einmal zu einem bestimmten Zeitpunkt auslöst.
        // Geben Sie -1 als Intervallwert ein, um das wiederholte Auslösen des
        // Timers zu unterbinden.
        new Timer(handler, null, waitTime, new TimeSpan(-1));
    }

    public static void Main(string[] args) {

        DateTime execTime;

        // Sicherstellen, dass ein Ausführungszeitpunkt über die Kommandozeile bestimmt wurde.
        if (args.Length > 0) {

            // Den String ins DateTime-Format umwandeln. Nur RFC1123-Zeitformat wird untersützt.
            try {

                execTime = DateTime.ParseExact(args[0],"r", null);

                Console.WriteLine("Aktueller Zeitpunkt   : " +
                    DateTime.Now.ToString("r"));

                Console.WriteLine("Ausführungszeitpunkt  : " +
                    execTime.ToString("r"));

                RunAt(execTime);

            } catch (FormatException) {
 
                Console.WriteLine("Ausführungszeitpunkt muss im folgenden Format eingegeben werden:" +
                    " ddd, dd MMM yyyy HH':'mm':'ss 'GMT'");
            }

            // Wait to continue.
            Console.WriteLine("Hauptmethode abgeschlossen. Drücken Sie Return.");
            Console.ReadLine();

        } else {

 
            Console.WriteLine("Geben Sie den Zeitpunkt an, zu dem Sie" +
                " die Ausführung starten wollen (im folgenden Format) : " + 
                " ddd, dd MMM yyyy HH':'mm':'ss 'GMT'");
        }
    }
}

Ausführung einer Methode durch den Zustandswechsel eines WaitHandle-Objektes

Aufgabe
Sie müssen eine oder mehrere Methoden automatisch starten, wenn ein Objekt, das von System.Threading.WaitHandle abgeleitet ist, einen Zustandswechsel vollzieht.

Lösung
Erstellen Sie eine System.Threading.WaitOrTimerCallback-Delegateninstanz, die die Methode referenziert, die Sie ausführen wollen. Registrieren Sie die Delegateninstanz und das WaitHandle-Objekt, das die Ausführung auslöst, mit Hilfe des Thread-Pools, indem Sie die statische Methode ThreadPool.RegisterWaitForSingleObject verwenden.

Beschreibung

Sie können die Klassen verwenden, die von der WaitHandle-Klasse abgeleitet sind (wie im Abschnitt: " Eine Methode asynchron ausführen " besprochen). Indem Sie die RegisterWaitForSingleObject-Methode der ThreadPool-Klasse verwenden, können Sie eine WaitForTimerCallback-Delegateninstanz für die Ausführung auf einem Thread-Pool-Thread registrieren, sobald ein zuvor spezifiziertes, von WaitHandle abgeleitetes Objekt den Signalzustand wechselt. Sie können den Thread-Pool so konfigurieren, dass die Methode nur ein einziges Mal ausgeführt wird oder dass sie sich automatisch neu für die Ausführung registriert, und zwar jedes Mal, wenn der Signalzustand gewechselt wird. Falls sich WaitHandle sowieso schon im signalisierten Zustand befindet, wenn Sie RegisterWaitForSingleObject aufrufen, wird die Methode unmittelbar ausgeführt.

Die Unregister-Methode des System.Threading.RegisteredWaitHandle-Objektes, das die RegisterWaitForSingleObjekt-Methode zurückgeliefert, wird dazu verwendet, eine registrierte Warteoperationen im Bedarfsfall abzubrechen.

Die Klasse, die für die Steuerung auf häufigsten verwendet wird, ist AutoResetEvent, die automatisch in einen unsignalisierten Zustand zurückwechselt, nachdem dieser signalisiert wurde. Sie können aber dennoch auch Alternativen wie die ManualResetEvent-Klasse und die Mutex-Klasse verwenden, die es erforderlich machen, den Signalzustand manuell zu wechseln. Das folgende Beispiel demonstriert den Einsatz eines AutoResetEvent-Objektes, um die Ausführung einer Methode namens EventHandler zu steuern.

using System;
using System.Threading;

public class EventExecutionExample {

    // Eine Methode, die ausgeführt wird, wenn das AutoResetEvent-Objekt
    // zum Zustand "signalisiert" wechselt oder wenn die Warteroutine
    // die Timeout-Schwelle erreicht hat.
    private static void EventHandler(object state, bool timedout) {

        // Eine entsprechende Nachricht im Konsolenfenster ausgeben,
        // basierend auf dem Ereignis - Timeout beim Warten oder signalisiertes 
        // AutoResetEvent.
        if (timedout) {
            
            Console.WriteLine("{0} : Bis zum Timeout gewartet.",
                DateTime.Now.ToString("HH:mm:ss.ffff"));
        } else {

            Console.WriteLine("{0} : {1}", 
                DateTime.Now.ToString("HH:mm:ss.ffff"), state);
        }
    }

    public static void Main() {

        // Ein neues AutoResetEvent-Objekt im unsignalisierten Zustand erstellen.
        AutoResetEvent autoEvent = new AutoResetEvent(false);

        // Erstellen einer WaitOrTimerCallback-Delegateninstanz, die
        // die statische Methode EventHandler referenziert. EventHandler
        // wird aufgerufen, wenn das AutoResetEvent-Objekt in den signalisierten
        // Zustand wechselt oder das Warten die Timeout-Zeitspanne überschritten hat.
        WaitOrTimerCallback handler = 
            new WaitOrTimerCallback(EventHandler);

        // Erstellen des Zustandobjektes, dass der Ereignisbehandlungsroutine 
        // übergeben wird, wenn die Methode ausgelöst wird. In diesem
        // Fall eine anzuzeigende Nachricht.
        string state = "AutoResetEvent im signalisierten Zustand.";

        // Die Delegateninstanz so konfigurieren, dass sie wartet, 
        // bis AutoResetEvent in den signalisierten Zustand wechselt.
        // Ein Timeout von 3 Sekunden definieren, und die Warteroutine
        // so konfigurieren, dass sie sich automatisch in den Ausgangszustand zurückversetzt.
        // (letzter Partameter).
        RegisteredWaitHandle handle = 
            ThreadPool.RegisterWaitForSingleObject(autoEvent, handler, 
            state, 3000, false);

        Console.WriteLine("Drücken Sie Return, um AutoResetEvent in den signalisierten Zustand zu versetzen,\n" +
            " oder geben Sie \"Abbruch\" ein, um die Warteroutine zu deregistrieren (beenden).");

        while (Console.ReadLine().ToUpper() != "ABBRUCH") {

            // Falls "Abbruch" nicht über die Konsole eingegeben wurde,
            // AutoResetEvent in den signalisierten Zustand versetzen, was dazu führt,
            // dass EventHandler ausgeführt wird. Das AutoResetEvent-Objekt
            // versetzt sich danach automatisch wieder in den unsignalisierten Zustand.
            autoEvent.Set();
        }

        // Die WarteRoutine deregistrieren.
        Console.WriteLine("Deregistrieren der Warteroutine.");
        handle.Unregister(null);

        // Auf Programmende warten.
        Console.WriteLine("Hauptroutine abgeschlossen. Drücken Sie Return.");
        Console.ReadLine();
    }
}

Eine Methode unter Verwendung eines neuen Threads ausführen

Aufgabe
Sie müssen bestimmten Code in einem eigenen Thread ausführen, und Sie möchten die absolute Kontrolle über den Thread-Status und die Thread-Ausführung haben.

Lösung
Deklarieren Sie eine Methode, die void zurückliefert und keine Parameter übernimmt. Erstellen Sie eine System.Threading.ThreadStart-Delegateninstanz, die diese Methode referenziert. Erstellen Sie ein neues System.Threading.Thread-Objekt, und übergeben Sie die Delegateninstanz als Parameter ihrem Konstruktor. Rufen Sie die Thread.Start-Methode auf, um die Ausführung Ihrer Methode zu starten.

Beschreibung

Für ein Maximum an Kontrolle und Flexibilität beim Erstellen von Multithreading-Anwendungen müssen Sie selbst die Verantwortung bei der Erstellung und Verwaltung von Threads übernehmen. Dies ist der komplexeste Ansatz bei der Multithreading-Programmierung, aber gleichzeitig auch der einzige Weg, die Restriktionen und Einschränkungen zu überwinden, wie sie bei der Programmierung mit Thread-Pool-Threads auftauchen und wie sie Ihnen bei den vergangenen vier Rezepten schon begegnet sind. Die Thread-Klasse stellt Ihnen den Mechanismus zur Verfügung, durch den Sie Threads erstellen und steuern können. Um einen neuen Thread zu erstellen und anschließend zu starten, verfahren Sie nach folgendem Verfahren:

  1. Erstellen Sie eine ThreadStart-Delegateninstanz, die auf die Methode verweist, die wiederum den Code enthält, den Sie als eigenständigen Thread ausführen möchten. Wie bei jedem anderen Delegaten, kann ThreadStart eine statische oder eine Instanz-Methode referenzieren. Die referenzierte Methode darf keine Parameter übernehmen und muss void zurückliefern.

  2. Erstellen Sie ein neues Thread-Objekt, und übergeben Sie die ThreadStart-Delegateninstanz als Parameter dem Thread-Konstruktor. Der neue Thread hat Unstarted (ein Member der System.Threading.ThreadState-Enumeration) als Ausgangsstatus.

  3. Rufen Sie die Start-Methode des Thread-Objektes auf, die dessen Zustand in ThreadState.Running ändert und die Ausführung der Methode startet, die durch die ThreadStart-Delegateninstanz referenziert wird. (Falls Sie die Start-Methode mehr als einmal aufrufen, wird eine System.Threading.ThreadStateException ausgelöst.)

Da der ThreadStart-Delegat ohne Parameter deklariert wird, können Sie keine Daten direkt zur referenzierten Methode übergeben. Um Daten einem neuen Thread zu übergeben, müssen Sie die Daten in einer Form zur Verfügung stellen, dass sie vom Code des neu erstellten Threads aus zugreifbar sind. Die übliche Vorgehensweise dazu ist, eine Klasse zu deklarieren, die sowohl den Thread-Code als auch die Daten zur Verfügung stellt, die vom Thread bearbeitet werden sollen. Wenn Sie einen neuen Thread starten wollen, erstellen Sie einfach eine Instanz dieses Container-Objektes, konfigurieren dessen Zustand und starten anschließend den neuen Thread. Ein Beispiel dazu finden Sie im Folgenden:

using System;
using System.Threading;

public class ThreadExample {

    // Private Member-Variable speichert den Zustand für die Verwendung durch neuen Thread.
    private int iterations;
    private string message;
    private int delay;

    public ThreadExample(int iterations, string message, int delay) {

        this.iterations = iterations;
        this.message = message;
        this.delay = delay;
    }

    public void Start() {

        // Eine ThreadStart-Delegateninstanz erstellen, die DisplayMessage referenziert.
        ThreadStart method = new ThreadStart(this.DisplayMessage);

        // Erstellen eines neuen Thread-Objektes, und seinem Konstruktor
        // die ThreadStart-Delegateninstanz übergeben.
        Thread thread = new Thread(method);

        Console.WriteLine("{0} : Neuen Thread starten.",  
            DateTime.Now.ToString("HH:mm:ss.ffff"));

        // Neuen Thread starten
        thread.Start();
    }

    private void DisplayMessage() {

        // Die Nachricht sooft wie angegeben im Konsolenfenster ausgeben;
        // zwischen den Ausgaben für die angegebene Zeit warten.
        for (int count = 0; count < iterations; count++) {

            Console.WriteLine("{0} : {1}",  
                DateTime.Now.ToString("HH:mm:ss.ffff"), message);

            // Thread für die angegebene Zeit aussetzen.
            Thread.Sleep(delay);
        }
    }

    public static void Main() {

        // Ein neues ThreadExample-Objekt erstellen.
        ThreadExample example = 
            new ThreadExample(5, "Ein Thread-Beispiel.", 500);

        // Das Thread-Beispielobjekt starten.
        example.Start();

        // Mit der weiteren Verarbeitung fortfahren.
        for (int count = 0; count < 13; count++) {
            Console.WriteLine("{0} : Verarbeitung fortführen...", 
                DateTime.Now.ToString("HH:mm:ss.ffff"));
            Thread.Sleep(200);
        }

        Console.WriteLine("Hauptmethode abgeschlossen. Drücken Sie Return.");
        Console.ReadLine();
    }
}

Anzeigen: