Exemplarische Vorgehensweise: Erstellen einer einfachen Multithreadkomponente mit Visual C#

Die BackgroundWorker-Komponente ersetzt den System.Threading-Namespace und erweitert diesen um Funktionen, jedoch wird der System.Threading-Namespace aus Gründen der Abwärtskompatibilität und, falls gewünscht, für die zukünftige Verwendung beibehalten. Weitere Informationen finden Sie unter Übersicht über die BackgroundWorker-Komponente.

Sie können Anwendungen schreiben, die in der Lage sind, mehrere Aufgaben gleichzeitig auszuführen. Diese Fähigkeit, die Multithreading oder freies Threading genannt wird, ist eine leistungsstarke Methode, um rechenintensive Komponenten zu entwickeln, die Benutzereingaben benötigen. Ein Beispiel für eine Komponente, die sich Multithreading zunutze machen könnte, wäre eine Komponente, die Berechnungen in Lohnabrechnungen durchführt. Die Komponente könnte in einem Thread die vom Benutzer in eine Datenbank eingegebenen Daten verarbeiten, während ein anderer Thread die rechenintensiven Berechnungen für die Lohnabrechnung übernimmt. Da diese Prozesse in verschiedenen Threads ausgeführt werden, müssen Benutzer nicht warten, bis der Computer die Berechnungen beendet hat, um weitere Daten eingeben zu können. In dieser exemplarischen Vorgehensweise erstellen Sie eine einfache Multithreadkomponente, die verschiedene komplexe Berechnungen gleichzeitig durchführt.

Erstellen des Projekts

Die Beispielanwendung besteht aus einem einzelnen Formular und einer Komponente. Der Benutzer gibt Werte ein und signalisiert der Komponente, mit den Berechnungen zu beginnen. Das Formular empfängt dann Werte von der Komponente und zeigt sie in Label-Steuerelementen an. Die Komponente führt die rechenintensiven Berechnungen durch und signalisiert dem Formular das Ende der Berechnungen. Sie erstellen in der Komponente öffentliche Variablen für die Speicherung von Werten, die über die Benutzeroberfläche eingegeben wurden. Außerdem implementieren Sie Methoden, die basierend auf den Werten dieser Variablen die Berechnungen durchführen.

Tipp

Zwar wäre normalerweise für eine Methode, die einen Wert berechnet, eine Funktion vorzuziehen, allerdings können dann zwischen Threads weder Parameter übergeben noch Werte zurückgegeben werden. Es gibt eine Reihe von einfachen Möglichkeiten, Werte an Threads zu übergeben und Werte von ihnen zurückzuerhalten. In diesem Beispiel geben Sie Werte an die Benutzeroberfläche zurück, indem Sie Public-Variablen aktualisieren; das Hauptprogramm wird mithilfe von Ereignissen benachrichtigt, nachdem die Threadausführung beendet ist.

Je nach den aktiven Einstellungen oder der Version unterscheiden sich die Dialogfelder und Menübefehle auf Ihrem Bildschirm möglicherweise von den in der Hilfe beschriebenen. Klicken Sie im Menü Extras auf Einstellungen importieren und exportieren, um die Einstellungen zu ändern. Weitere Informationen finden Sie unter Arbeiten mit Einstellungen.

So erstellen Sie das Formular

  1. Erstellen Sie ein neues Projekt vom Typ Windows-Anwendung.

  2. Nennen Sie die Anwendung Calculations, und benennen Sie Form1.cs in frmCalculations.cs um. Wenn Sie von Visual Studio aufgefordert werden, das Form1-Codeelement umzubenennen, klicken Sie auf Ja.

    Dieses Formular dient als primäre Benutzeroberfläche der Anwendung.

  3. Fügen Sie dem Formular fünf Label-Steuerelemente, vier Button-Steuerelemente und ein TextBox-Steuerelement hinzu.

  4. Legen Sie die Eigenschaften dieser Steuerelemente wie folgt fest:

    Steuerelement

    Name

    Text

    label1

    lblFactorial1

    (leer)

    label2

    lblFactorial2

    (leer)

    label3

    lblAddTwo

    (leer)

    label4

    lblRunLoops

    (leer)

    label5

    lblTotalCalculations

    (leer)

    button1

    btnFactorial1

    Fakultät

    button2

    btnFactorial2

    Fakultät - 1

    button3

    btnAddTwo

    Zwei addieren

    button4

    btnRunLoops

    Schleife ausführen

    textBox1

    txtValue

    (leer)

So erstellen Sie die Calculator-Komponente

  1. Klicken Sie im Menü Projekt auf Komponente hinzufügen.

  2. Nennen Sie diese Komponente Calculator.

So fügen Sie der Calculator-Komponente öffentliche Variablen hinzu

  1. Öffnen Sie den Code-Editor für Calculator.

  2. Fügen Sie Anweisungen hinzu, um öffentliche Variablen zu erstellen, mit denen Sie Werte von frmCalculations an die jeweiligen Threads übergeben können.

    Die varTotalCalculations-Variable zählt fortlaufend die Gesamtzahl der von der Komponente durchgeführten Berechnungen, und die anderen Variablen erhalten ihre Werte vom Formular.

    public int varAddTwo; 
    public int varFact1;
    public int varFact2;
    public int varLoopValue;
    public double varTotalCalculations = 0;
    

So fügen Sie der Calculator-Komponente Methoden und Ereignisse hinzu

  1. Deklarieren Sie die Delegaten für die Ereignisse, mit denen die Komponente Werte an das Formular zurückgibt.

    Tipp

    Obwohl vier Ereignisse deklariert werden, müssen nur drei Delegaten erstellt werden, da zwei der Ereignisse die gleiche Signatur haben.

    Fügen Sie unmittelbar unter die soeben eingegebenen Variablendeklarationen folgenden Code ein:

    // This delegate will be invoked with two of your events.
    public delegate void FactorialCompleteHandler(double Factorial, double TotalCalculations);
    public delegate void AddTwoCompleteHandler(int Result, double TotalCalculations);
    public delegate void LoopCompleteHandler(double TotalCalculations, int Counter);
    
  2. Deklarieren Sie die Ereignisse, die von der Komponente zur Kommunikation mit der Anwendung verwendet werden. Fügen Sie dazu unmittelbar unter dem im vorherigen Schritt eingegebenen Code folgenden Code ein:

    public event FactorialCompleteHandler FactorialComplete;
    public event FactorialCompleteHandler FactorialMinusOneComplete;
    public event AddTwoCompleteHandler AddTwoComplete;
    public event LoopCompleteHandler LoopComplete;
    
  3. Fügen Sie unmittelbar unter dem im vorherigen Schritt eingegebenen Code folgenden Code ein:

    // This method will calculate the value of a number minus 1 factorial
    // (varFact2-1!).
    public void FactorialMinusOne()
    {
       double varTotalAsOfNow = 0;
       double varResult = 1;
       // Performs a factorial calculation on varFact2 - 1.
       for (int varX = 1; varX <= varFact2 - 1; varX++)
       {
          varResult *= varX;
          // Increments varTotalCalculations and keeps track of the current 
          // total as of this instant.
          varTotalCalculations += 1;
          varTotalAsOfNow = varTotalCalculations;
       }
       // Signals that the method has completed, and communicates the 
       // result and a value of total calculations performed up to this 
       // point.
       FactorialMinusOneComplete(varResult, varTotalAsOfNow);
    }
    
    // This method will calculate the value of a number factorial.
    // (varFact1!)
    public void Factorial()
    {
       double varResult = 1;
       double varTotalAsOfNow = 0;
       for (int varX = 1; varX <= varFact1; varX++)
       {
          varResult *= varX;
          varTotalCalculations += 1;
          varTotalAsOfNow = varTotalCalculations;
       }
       FactorialComplete(varResult, varTotalAsOfNow);
    }
    
    // This method will add two to a number (varAddTwo+2).
    public void AddTwo()
    {
       double varTotalAsOfNow = 0;  
       int varResult = varAddTwo + 2;
       varTotalCalculations += 1;
       varTotalAsOfNow = varTotalCalculations;
       AddTwoComplete(varResult, varTotalAsOfNow);
    }
    
    // This method will run a loop with a nested loop varLoopValue times.
    public void RunALoop()
    {
       int varX;
       double varTotalAsOfNow = 0;
       for (varX = 1; varX <= varLoopValue; varX++)
       {
        // This nested loop is added solely for the purpose of slowing down
        // the program and creating a processor-intensive application.
          for (int varY = 1; varY <= 500; varY++)
          {
             varTotalCalculations += 1;
             varTotalAsOfNow = varTotalCalculations;
          }
       }
       LoopComplete(varTotalAsOfNow, varLoopValue);
    }
    

Übermitteln von Benutzereingaben an die Komponente

Im nächsten Schritt fügen Sie frmCalculations Code hinzu, um Benutzereingaben zu empfangen und Werte an die Calculator-Komponente zu übermitteln bzw. von der Komponente zu empfangen.

So implementieren Sie Front-End-Funktionalität für "frmCalculations"

  1. Öffnen Sie frmCalculations im Code-Editor.

  2. Suchen Sie die public partial class frmCalculations-Anweisung. Geben Sie unmittelbar unter { Folgendes ein:

    Calculator Calculator1;
    
  3. Suchen Sie den Konstruktor. Fügen Sie unmittelbar vor } die folgende Zeile ein:

    // Creates a new instance of Calculator.
    Calculator1 = new Calculator();
    
  4. Klicken Sie im Designer auf jede Schaltfläche, um die Codegliederung für die Click-Ereignishandler jedes Steuerelements zu generieren, und fügen Sie Code hinzu, um die Handler zu erstellen.

    Anschließend sollten die Click-Ereignishandler ungefähr folgendermaßen aussehen:

    // Passes the value typed in the txtValue to Calculator.varFact1.
    private void btnFactorial1_Click(object sender, System.EventArgs e)
    {
       Calculator1.varFact1 = int.Parse(txtValue.Text);
       // Disables the btnFactorial1 until this calculation is complete.
       btnFactorial1.Enabled = false;
       Calculator1.Factorial();
    }
    
    private void btnFactorial2_Click(object sender, System.EventArgs e)
    {
       Calculator1.varFact2 = int.Parse(txtValue.Text);
       btnFactorial2.Enabled = false;
       Calculator1.FactorialMinusOne();
    }
    private void btnAddTwo_Click(object sender, System.EventArgs e)
    {
       Calculator1.varAddTwo = int.Parse(txtValue.Text);
       btnAddTwo.Enabled = false;
       Calculator1.AddTwo();
    }
    private void btnRunLoops_Click(object sender, System.EventArgs e)
    {
       Calculator1.varLoopValue = int.Parse(txtValue.Text);
       btnRunLoops.Enabled = false;
       // Lets the user know that a loop is running
       lblRunLoops.Text = "Looping";
       Calculator1.RunALoop();
    }
    
  5. Geben Sie nach dem im vorherigen Schritt eingefügten Code folgenden Code ein, um die Ereignisse zu behandeln, die das Formular von Calculator1 empfängt:

    private void FactorialHandler(double Value, double Calculations)
    // Displays the returned value in the appropriate label.
    {
       lblFactorial1.Text = Value.ToString();
       // Re-enables the button so it can be used again.
       btnFactorial1.Enabled = true;
       // Updates the label that displays the total calculations performed
       lblTotalCalculations.Text = "TotalCalculations are " + 
          Calculations.ToString();
    }
    
    private void FactorialMinusHandler(double Value, double Calculations)
    {
       lblFactorial2.Text = Value.ToString();
       btnFactorial2.Enabled = true;
       lblTotalCalculations.Text = "TotalCalculations are " + 
          Calculations.ToString();
    }
    
    private void AddTwoHandler(int Value, double Calculations)
    {
       lblAddTwo.Text = Value.ToString();
       btnAddTwo.Enabled = true;
       lblTotalCalculations.Text = "TotalCalculations are " +
          Calculations.ToString();
    }
    
    private void LoopDoneHandler(double Calculations, int Count)
    {
       btnRunLoops.Enabled = true;
       lblRunLoops.Text = Count.ToString();
       lblTotalCalculations.Text = "TotalCalculations are " +
          Calculations.ToString();
    }
    
  6. Fügen Sie im Konstruktor von frmCalculations den folgenden Code direkt vor } hinzu, um die benutzerdefinierten Ereignisse zu behandeln, die das Formular von Calculator1 empfängt.

    Calculator1.FactorialComplete += new
       Calculator.FactorialCompleteHandler(this.FactorialHandler);
    Calculator1.FactorialMinusOneComplete += new
       Calculator.FactorialCompleteHandler(this.FactorialMinusHandler);
    Calculator1.AddTwoComplete += new
       Calculator.AddTwoCompleteHandler(this.AddTwoHandler);
    Calculator1.LoopComplete += new
       Calculator.LoopCompleteHandler(this.LoopDoneHandler);
    

Testen der Anwendung

Sie haben nun ein Projekt erstellt, das ein Formular und eine Komponente zur Durchführung mehrerer komplexer Berechnungen enthält. Obwohl die Multithreadingfunktionalität noch nicht implementiert wurde, sollten Sie, bevor Sie fortfahren, das Projekt testen, um sicherzustellen, dass es korrekt arbeitet.

So testen Sie das Projekt

  1. Klicken Sie im Menü Debuggen auf Debuggen starten.

    Die Anwendung wird gestartet, und frmCalculations wird angezeigt.

  2. Geben Sie in das Textfeld 4 ein, und klicken Sie dann auf die Schaltfläche Add two.

    In der Bezeichnung darunter sollte nun die Zahl "6" angezeigt werden, und in lblTotalCalculations sollte "Total Calculations are 1" angezeigt werden.

  3. Klicken Sie nun auf die Schaltfläche Factorial - 1.

    Unter der Schaltfläche sollte die Zahl "6" angezeigt werden, und lblTotalCalculations sollte nun "Total Calculations are 4" lauten.

  4. Ändern Sie den Wert im Textfeld in 20, und klicken Sie dann auf die Schaltfläche Factorial.

    Darunter wird die Zahl "2,43290200817664E+18" angezeigt, und lblTotalCalculations lautet nun "Total Calculations are 24".

  5. Ändern Sie den Wert im Textfeld in 50000, und klicken Sie dann auf die Schaltfläche Run A Loop.

    Beachten Sie, dass Sie etwas warten müssen, bevor diese Schaltfläche wieder aktiviert wird. In der Beschriftung unter dieser Schaltfläche sollte "50000" angezeigt werden, und die Gesamtanzahl der Berechnungen beträgt "25000024".

  6. Ändern Sie den Wert im Textfeld in 5000000, und klicken Sie zunächst auf die Schaltfläche Run A Loop und unmittelbar danach auf die Schaltfläche Add Two. Klicken Sie erneut darauf.

    Die Schaltfläche sowie alle anderen Steuerelemente im Formular reagieren erst wieder, nachdem die Schleifendurchläufe beendet wurden.

    Wenn ein Programm mit nur einem Thread arbeitet, wird das Programm durch rechenintensive Vorgänge wie im oben genannten Beispiel häufig so lange blockiert, bis sie vollständig ausgeführt sind. Im nächsten Abschnitt wird der Anwendung die Multithreadfunktionalität hinzugefügt, sodass mehrere Threads gleichzeitig ausgeführt werden können.

Hinzufügen der Multithreadfunktionalität

Im vorigen Beispiel wurden die Grenzen von Anwendungen aufgezeigt, die mit nur einem Thread ausgeführt werden. Im nächsten Abschnitt werden der Komponente mithilfe der Thread-Klasse mehrere Ausführungsthreads hinzugefügt.

So fügen Sie die Unterroutine "Threads" hinzu

  1. Öffnen Sie Calculator.cs im Code-Editor.

  2. Suchen Sie im Anfangsabschnitt des Codes die Klassendeklaration, und geben Sie unmittelbar unter { Folgendes ein:

    // Declares the variables you will use to hold your thread objects.
    public System.Threading.Thread FactorialThread; 
    public System.Threading.Thread FactorialMinusOneThread;  
    public System.Threading.Thread AddTwoThread; 
    public System.Threading.Thread LoopThread;
    
  3. Fügen Sie unmittelbar vor dem Ende der Klassendeklaration am Ende des Codes die folgende Methode ein:

    public void ChooseThreads(int threadNumber)
    {
    // Determines which thread to start based on the value it receives.
    switch(threadNumber)
       {
          case 1:
             // Sets the thread using the AddressOf the subroutine where
             // the thread will start.
             FactorialThread = new System.Threading.Thread(new
                System.Threading.ThreadStart(this.Factorial));
             // Starts the thread.
             FactorialThread.Start();
             break;
          case 2:
             FactorialMinusOneThread = new
                System.Threading.Thread(new
                   System.Threading.ThreadStart(this.FactorialMinusOne));
             FactorialMinusOneThread.Start();
             break;
          case 3:
             AddTwoThread = new System.Threading.Thread(new
                 System.Threading.ThreadStart(this.AddTwo));
             AddTwoThread.Start();
             break;
          case 4:
             LoopThread = new System.Threading.Thread(new
                System.Threading.ThreadStart(this.RunALoop));
             LoopThread.Start();
             break;
       }
    }
    

    Für die Instanziierung eines Thread wird ein Argument in Form eines ThreadStart benötigt. Ein ThreadStart ist ein Delegat, der auf die Adresse der Methode verweist, bei der der Thread beginnen soll. Ein ThreadStart kann keine Parameter annehmen oder Werte übergeben und somit nur eine Methode vom Typ void angeben. Die ChooseThreads-Methode, die Sie eben implementiert haben, erhält einen Wert vom aufrufenden Programm und ermittelt aus diesem Wert den zu startenden Thread.

So fügen Sie "frmCalculations" den entsprechenden Code hinzu

  1. Öffnen Sie die Datei frmCalculations.cs im Code-Editor, und suchen Sie private void btnFactorial1_Click.

    1. Kommentieren Sie die Zeile aus, durch die die Calculator1.Factorial1-Methode direkt aufgerufen wird:

      // Calculator1.Factorial()
      
    2. Fügen Sie die folgende Zeile hinzu, um die Calculator1.ChooseThreads-Methode aufzurufen:

      // Passes the value 1 to Calculator1, thus directing it to start the 
      // correct thread.
      Calculator1.ChooseThreads(1);
      
  2. Nehmen Sie ähnliche Änderungen an den anderen button_click-Methoden vor.

    Tipp

    Stellen Sie sicher, dass für das Threads-Argument der richtige Wert angegeben wird.

    Anschließend sollte der Code ungefähr wie folgt aussehen:

    private void btnFactorial1_Click(object sender, System.EventArgs e)
    // Passes the value typed in the txtValue to Calculator.varFact1
    {
       Calculator1.varFact1 = int.Parse(txtValue.Text);
       // Disables the btnFactorial1 until this calculation is complete
       btnFactorial1.Enabled = false;
       // Calculator1.Factorial();
       Calculator1.ChooseThreads(1);
    }
    
    private void btnFactorial2_Click(object sender, System.EventArgs e)
    {
       Calculator1.varFact2 = int.Parse(txtValue.Text); 
       btnFactorial2.Enabled = false;         
       // Calculator1.FactorialMinusOne();
       Calculator1.ChooseThreads(2);
    }
    private void btnAddTwo_Click(object sender, System.EventArgs e)
    {
       Calculator1.varAddTwo = int.Parse(txtValue.Text);
       btnAddTwo.Enabled = false;
       // Calculator1.AddTwo();
       Calculator1.ChooseThreads(3);
    }
    
    private void btnRunLoops_Click(object sender, System.EventArgs e)
    {
       Calculator1.varLoopValue = int.Parse(txtValue.Text);
       btnRunLoops.Enabled = false;
       // Lets the user know that a loop is running
       lblRunLoops.Text = "Looping";
       // Calculator1.RunALoop();
       Calculator1.ChooseThreads(4);
    }
    

Marshallingaufrufe an Steuerelemente

Als Nächstes wird die Aktualisierung der Formularanzeige verbessert. Da Steuerelemente stets dem Hauptausführungsthread zugehörig sind, ist für jeden Aufruf eines Steuerelements von einem untergeordneten Thread ein Marshalling-Aufruf erforderlich. Als Marshallen bezeichnet man das Weiterleiten eines Aufrufs über Threadgrenzen hinweg. Dieser Vorgang ist sehr systemressourceintensiv. Um die Anzahl der erforderlichen Marshallingvorgänge zu minimieren und die threadsichere Behandlung der Aufrufe sicherzustellen, rufen Sie mit der Control.BeginInvoke-Methode Methoden im Hauptausführungsthread auf. Auf diese Weise wird die Anzahl der erforderlichen Marshallingvorgänge, die über Threadgrenzen hinausgehen, eingeschränkt. Diese Art von Aufruf ist notwendig, wenn Methoden aufgerufen werden sollen, die Auswirkungen auf Steuerelemente haben. Ausführliche Informationen finden Sie unter Gewusst wie: Bearbeiten von Steuerelementen aus Threads.

So erstellen Sie Prozeduren zum Aufrufen von Steuerelementen

  1. Öffnen Sie den Code-Editor für frmCalculations. Fügen Sie im Deklarationsabschnitt folgenden Code hinzu:

    public delegate void FHandler(double Value, double Calculations);
    public delegate void A2Handler(int Value, double Calculations);
    public delegate void LDHandler(double Calculations, int Count);
    

    Invoke und BeginInvoke benötigen als Argument einen Delegaten für die entsprechende Methode. Diese Zeilen deklarieren die Delegatsignaturen, mit denen BeginInvoke die entsprechenden Methoden aufruft.

  2. Fügen Sie dem Code die folgenden leeren Methoden hinzu.

    public void FactHandler(double Value, double Calculations)
    {
    }
    public void Fact1Handler(double Value, double Calculations)
    {
    }
    public void Add2Handler(int Value, double Calculations)
    {
    }
    public void LDoneHandler(double Calculations, int Count)
    {
    }
    
  3. Klicken Sie im Menü Bearbeiten auf Ausschneiden und Einfügen, um den gesamten Code aus der FactorialHandler -Methode auszuschneiden und in FactHandler einzufügen.

  4. Wiederholen Sie den vorherigen Schritt für FactorialMinusHandler und Fact1Handler, AddTwoHandler und Add2Handler sowie LoopDoneHandler und LDoneHandler.

    Danach sollten FactorialHandler, Factorial1Handler, AddTwoHandler und LoopDoneHandler keinen Code mehr enthalten, und der gesamte Code sollte nun in die entsprechenden neuen Methoden verschoben worden sein.

  5. Rufen Sie die BeginInvoke-Methode auf, um die Methoden asynchron aufzurufen. Sie können BeginInvoke entweder vom Formular (this) oder von jedem beliebigen Steuerelement im Formular aufrufen.

    Anschließend sollte der Code ungefähr wie folgt aussehen:

    protected void FactorialHandler(double Value, double Calculations)
    {
       // BeginInvoke causes asynchronous execution to begin at the address
       // specified by the delegate. Simply put, it transfers execution of 
       // this method back to the main thread. Any parameters required by 
       // the method contained at the delegate are wrapped in an object and 
       // passed. 
       this.BeginInvoke(new FHandler(FactHandler), new Object[]
          {Value, Calculations});
    }
    protected void FactorialMinusHandler(double Value, double Calculations)
    {
       this.BeginInvoke(new FHandler(Fact1Handler), new Object []
          {Value, Calculations});
    }
    
    protected void AddTwoHandler(int Value, double Calculations)
    {
       this.BeginInvoke(new A2Handler(Add2Handler), new Object[]
          {Value, Calculations});
    }
    
    protected void LoopDoneHandler(double Calculations, int Count)
    {
       this.BeginInvoke(new LDHandler(LDoneHandler), new Object[]
          {Calculations, Count});
    }
    

    Auf den ersten Blick sieht es so aus, als ob der Ereignishandler einfach die nächste Methode aufruft. Tatsächlich bewirkt der Ereignishandler jedoch, dass eine Methode im Hauptausführungsthread aufgerufen wird. Diese Vorgehensweise vermeidet Aufrufe über Threadgrenzen hinweg. Die Multithreadanwendung arbeitet daher effizient und ohne zu blockieren. Ausführliche Informationen zum Arbeiten mit Steuerelementen in einer Multithreadumgebung finden Sie unter Gewusst wie: Bearbeiten von Steuerelementen aus Threads.

  6. Speichern Sie Ihre Arbeit.

  7. Testen Sie die Projektmappe, indem Sie im Menü Debuggen die Option Debuggen starten auswählen.

    1. Geben Sie in das Textfeld 10000000 ein, und klicken Sie auf Run A Loop.

      Unter dieser Schaltfläche wird die Bezeichnung "Looping" angezeigt. Die Ausführung dieser Schleife sollte einige Zeit in Anspruch nehmen. Geben Sie eine höhere Zahl ein, falls die Schleife zu schnell beendet wird.

    2. Klicken Sie in schneller Folge auf alle drei Schaltflächen, die noch aktiviert sind. Sie werden feststellen, dass alle Schaltflächen auf Ihre Eingabe reagieren. Die Bezeichnung unter Add Two sollte als Erste ein Ergebnis anzeigen. In den Bezeichnungen unter den Factorial-Schaltflächen werden die Ergebnisse etwas später angezeigt. Diese Ergebnisse gehen gegen Unendlich, da das Ergebnis der Fakultät von 10.000.000 für eine Variable des Typs double zu groß ist. Nach einer weiteren Verzögerung werden schließlich die Ergebnisse unter der Schaltfläche Run A Loop zurückgegeben.

      Wie Sie feststellen konnten, wurden in vier getrennten Threads gleichzeitig vier verschiedene Berechnungen ausgeführt. Die Benutzeroberfläche reagierte weiterhin auf Eingaben, und die Ergebnisse wurden zurückgegeben, nachdem der jeweilige Thread die Berechnung abgeschlossen hatte.

Koordinieren von Threads

Erfahrenen Benutzern von Multithreadanwendungen wird im zuvor eingegebenen Code eine kleine Ungenauigkeit aufgefallen sein. Alle Berechnungsmethoden in Calculator weisen die folgenden Codezeilen auf:

varTotalCalculations += 1;
varTotalAsOfNow = varTotalCalculations;

Diese beiden Zeilen erhöhen den Wert der öffentlichen Variablen varTotalCalculations und legen die lokale Variable varTotalAsOfNow auf den gleichen Wert fest. Dieser Wert wird dann an frmCalculations zurückgegeben und in einem Label-Steuerelement angezeigt. Aber wird auch der richtige Wert zurückgegeben? Wenn nur ein einziger Ausführungsthread verwendet wird, kann dies ganz klar bejaht werden. Im Falle mehrerer Threads ist die Antwort allerdings weniger eindeutig. Jeder Thread hat die Fähigkeit, den Wert der Variablen varTotalCalculations zu erhöhen. Es kann vorkommen, dass einer der Threads die Variable erhöht und gleich danach ein anderer Thread die Variable verändert, noch bevor der erste Thread den Wert in die Variable varTotalAsOfNow übertragen kann. Dadurch besteht die Gefahr, dass die einzelnen Threads tatsächlich falsche Ergebnisse liefern. Visual C# stellt die lock-Anweisung (C#-Referenz) bereit, die die Synchronisierung der Threads ermöglicht und damit sicherstellt, dass jeder Thread stets ein richtiges Ergebnis zurückgibt. Die Syntax für lock lautet wie folgt:

lock(AnObject)
{
   // Insert code that affects the object.
   // Insert more code that affects the object.
   // Insert more code that affects the object.
// Release the lock.
}

Wenn der lock-Block eingegeben wird, wird die Ausführung in dem angegebenen Ausdruck blockiert, bis für das fragliche Objekt eine exklusive Sperre für den angegebenen Thread vorliegt. Im oben gezeigten Beispiel wird die Ausführung für AnObject blockiert. lock muss mit Objekten verwendet werden, die statt eines Werts einen Verweis zurückgeben. Die Ausführung wird dann als Block fortgesetzt, ohne dass andere Threads Einfluss auf das Objekt nehmen können. Eine Reihe von Anweisungen, die als Einheit ausgeführt werden, wird als atomar bezeichnet. Nach Erreichen von } wird der Ausdruck freigegeben, und die Threads können ganz normal fortgesetzt werden.

So fügen Sie der Anwendung die lock-Anweisung hinzu

  1. Öffnen Sie Calculator.cs im Code-Editor.

  2. Suchen Sie alle Instanzen des folgenden Codes:

    varTotalCalculations += 1;
    varTotalAsOfNow = varTotalCalculations;
    

    Es müsste vier Instanzen dieses Codes geben, eine pro Berechnungsmethode.

  3. Ändern Sie diesen Code wie folgt:

    lock(this)
    {
       varTotalCalculations += 1;
       varTotalAsOfNow = varTotalCalculations;
    }
    
  4. Speichern Sie Ihre Arbeit, und testen Sie das Projekt wie im vorangegangenen Beispiel.

    Die Verarbeitungsgeschwindigkeit des Programms könnte etwas langsamer sein. Das liegt daran, dass die Ausführung der Threads angehalten wird, sobald ein Thread exklusiven Zugriff auf die Komponente erlangt hat. Sie sichern so zwar die Genauigkeit der Ergebnisse, doch geht dies zu Lasten der Ausführungsgeschwindigkeit, die mithilfe der Threads eigentlich verbessert werden sollte. Sie sollten sorgfältig abwägen, ob die Synchronisierung von Threads notwendig ist, und sie nur implementieren, wenn es zwingende Gründe dafür gibt.

Siehe auch

Aufgaben

Gewusst wie: Koordinieren mehrerer Ausführungsthreads

Exemplarische Vorgehensweise: Erstellen einer einfachen Multithreadkomponente mit Visual Basic

Referenz

BackgroundWorker

Konzepte

Übersicht über ereignisbasierte asynchrone Muster

Weitere Ressourcen

Programmieren mit Komponenten

Exemplarische Vorgehensweisen für das Programmieren von Komponenten

Multithreading in Komponenten