ProfilProfil
 Registrieren
 Login
Bild der WocheBild der Woche

(von Backslider)
Kommentare (0)
****

Weitere
User onlineBenutzer online
Gäste online: 9
Mitglieder online: SteveKr
Registrierte Mitglieder: 2117
Neustes Mitglied: vitapen

(Under Construction) Threads - Lock - Synchronisation

Gehe zu Seite 1, 2  Weiter
Neue Antwort erstellen
 

 

Autor Nachricht
 
 
Ysas
Member


Anmeldedatum: 07.05.2009
Beiträge: 265
Wohnort: Österreich

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 14:27    Titel: (Under Construction) Threads - Lock - Synchronisation

Threads - Lock - Synchronisation
von Ysas
Version 0.2

Hier findet ihr immer einen Link zur Aktuellsten Version! Und zum PDF Download

Einleitung
Im Ersten Teil des Tutorials geht es um Threading, wie man Threads erstellt und welche Klassen uns das .NET zu Verfügung stellt
Im zweiten Teil des Tutorials geht es um Threadsynchronisation und Locking! Am Anfang werde ich etwas detaillierter die Probleme behandeln die genauen Methoden dann aber nur kurz beschreiben und mit einem Beispiel erklären da dies sonst meinen Zeitrahmen sprengen würde!

Warum brauchen wir Threads ?
Was ist ein Thread eigentlich? Wozu braucht man sie und welche Vor und Nachteile haben sie. Das sind die Punkte über die man sich am Anfang im klaren sein muss.

Jede Applikation egal ob sie eine Konsolenanwendung, WindowsForms,WPF, Windows Service, .... ist alles besteht aus Threads. Mindestens aus einem! Wenn ihr bisher nicht explizit mit Threads gearbeitet habt dann ist der Thread den ihr kennt der Haupt-Thread (Mainthread). Der Hauptthread ist der Erste der gestartet wird und in der Regel der letzte der beendet wird. Er enthält den Programm Einsprungs Punkt also unsere void Main() Methode.
Die Meisten Programme würden auch ohne Threads gut funktionieren. Warum also braucht man Threads ? Auf diese Frage gibt es unzählige Antworten einige davon sind:

  • Durchführung von Arbeiten (Berechnungen, I/O ....) ohne das eigene Programm zu blockieren
  • Unabhängige Berechnungen parallel durchführen
  • Kommunikation mit anderen Systemen ohne das das Hauptprogramm blockiert wird.
  • Nutzung aller Systemressourcen
  • ...


Jeder Programmierer steht irgendwann mal vor so einem Problem:

Code:
foreach (var d in Data)
{
   SehrLangeBerechnung(d);
}

}
"SehrLangeBerechnung" führt für jedes object d eine unabhängige Berechnung durch. Hat man dieses Code Snipped z.b. in einer WindowsForms Applikation würde die Form "einfrieren" man kann nichts mehr klicken, darstellen etc... bis die Berechnung fertig ist. Man kann nicht mal einen Progressbar anzeigen um den User zu informieren das das Programm noch läuft. Sitzt man wie die meisten Programmierer jetzt noch vor einem High End Mehrprozessor Rechner sieht man im Taskmanager das das Programm gerade mal 25% (bei einem Quadcore) bzw 50% (DualCore) CPU Auslastung hat.

Warum ist das alles so?

Die Windows Applikation friert ein da die Schleife mit der Berechnung den HauptThread blockiert. Die Form wird nicht neu gezeichnet und
nach einigen Sekunden kommt sogar die Meldung das die Applikation nicht mehr reagiert. Windows Forms Applikationen "zeichnen" sich alle paar Millisekunden neu genauso wie DirectX Spiele oder andere Programme mit einer Graphischen Oberfläche. Es werden unzählige Events gefeuert wenn man z.b. mit der Maus über die Form fährt wenn man Tasten drückt usw.... all diese Dinge werden im HauptThread gemacht da nur dieser eine Thread mit dem Bildschirm interagieren kann!

Die 25% bzw 50% CPU Nutzung ist sehr leicht zu erklären. Ein Thread kann maximal einen CPU kern auslasten. Das bedeutet auf einer Einzel CPU Maschine sind das 100% je mehr CPU Kerne ein Rechner hat desto geringer würde euch die CPU Nutzung erscheinen wenn ihr nur einen Thread habt. Aktuelle Entwickler und Spiele PCs haben meist 4 CPU Kerne. Moderne Server haben meist 16 und mehr Kerne. Windows Server 2008 würde bis 512 CPU Kerne unterstützen. SuperComputer haben oft mehrere 1000 CPU Kerne.
Ich behaupte mal und ich denke das mir da keiner widerspricht, dass die Computer in den nächsten Jahren immer mehr CPU Kerne bekommen. Und genau deshalb ist es wichtig das wir jetzt schon unsere Programme darauf auslegen!


Threading in .NET
Also machen wir einfach ein paar Threads unsere Programm läuft dann auf nem Quadcore 4x so schnell und blockiert meine Anwendung nicht mehr....
Falsch! So einfach ist das ganze (noch) nicht.
Info hat Folgendes geschrieben:
Das "(noch)" bezieht sich auf .NET 4.0 wo wirklich einigen sehr viel einfacher wird dazu werde ich aber ein eigenes Tutorial veröffentlichen.

Zuerst sollten wir uns mal anschauen wie man einen Thread in .NET erstellt.
Alles das wir dafür brauchen finden wir in dem Namespace
Code:
using System.Threading;

In diesem Namespace finden sich einige Klassen und Interfaces. Die Meisten sind für Synchronisation und Locking zuständig und diese werden wir uns später genauer ansehen. Im Grunde gibt es zwei gängige Möglichkeiten einen Thread zu erstellen oder sagen wir vorerst mal etwas im Hintergrund auszuführen da nicht immer ein neuere Thread von nöten ist.

  • Asynchrone Delegates
  • Thread Klasse


Im .NET gibt es noch andere Methoden Threads zu erstellen aber in Grunde verwenden alle die Klasse System.Threading.Thread. Auch Asynchrone Delegates verwenden diese, aufgrund der Häufigkeit und der einfachen Verwendung möchte ich sie vorher behandeln.

Asynchrone Delegates
Wer an dieser Stelle nicht weis was ein Delegate ist sollte sich vorher ein Buch über C# besorgen und sich ein bisschen über die Grundlagen informieren. Für alle anderen noch zur Erinnerung: Ein delegate ist eine Typsichere Referenz zu einer Methode.

Hinweis: Asynchrone Delegates verwenden im Hintergrund einen ThreadPool! Diesen werden wir etwas später betrachten!

Für unser Beispiel werde ich eine kleine Berechnung simulieren.

Code:

       static int Calculate(int data,int ms)
       {
           Console.WriteLine("Berechnung wird gestartet");
           Thread.Sleep(ms); // Berechnung simulieren
           Console.WriteLine("Berechnung abgeschlossen");
           return ++data;
       }


Der Delegate für diese Methode muss die gleichen Parameter wie die Methode haben.

Code:

public delegate int CalculateDelegate(int data, int ms);

So jetzt stehen wir vor dem Ersten Problem! Würden wir jetzt einen Thread für die Berechnung in unserem Programm starten dann würde das Programm einfach weiter laufen und wir würden niemals das Ergebnis wissen. Es gibt viele Anwendungsbereiche wo uns das Ergebnis eines Threads nicht interessiert oder der Thread kümmert sich selber um die Ausgabe. Aber in unserem Fall wollen wir das Ergebnis gerne wissen! Da ein Thread aber im Hintergrund läuft wissen wir nicht wann die Berechnung abgeschlossen ist.

Wir haben jetzt mehrere Möglichkeiten zu unserem Ergebnis zu kommen.

Polling

Polling bedeutet das wir in regelmäßigen Abständen nachfragen ob der Delegate fertig ist. .NET stellt uns dafür ein Interface zu verfügung: IAsyncResult
Wir schreiben nun ein kleines Programm das die Berechnung im Hintergrund startet und alle 50ms wird nachgefragt ob ein Ergebnis vorliegt. Wärend wir warten geben wir dem Benutzer Feedback in dem wir Punkte in die Konsole ausgeben.

Code:

static void Main()
{
    CalculateDelegate d = Calculate;

    IAsyncResult ar = d.BeginInvoke(
            1,      //unser int data
            1000,   // unser int ms
            null,   // Async. Callback (mehr dazu später)
            null)// Zusätzliche Daten (mehr dazu später)

    while (!ar.IsCompleted)
    {
        Console.Write(".");
        Thread.Sleep(50); // 50ms warten
    }
    int result = d.EndInvoke(ar);
    Console.WriteLine("\nErgebnis:" + result);
}


Würden wir in dieser Konsolen Anwendung nicht warten bis die Berechnung fertig ist würde das Programm einfach durchlaufen und den Thread einfach wieder beenden.

Wait Handle
Eine sehr ähnliche Variante ist die verwendung eines Wait Handles. Ich werde das jetzt nur mit einem kurzen Beispiel zeigen da wir diese Methode später genauerer behandeln.

Code:

static void Main()
{
    CalculateDelegate d = Calculate;

    IAsyncResult ar = d.BeginInvoke(
            1,      //unser int data
            1000,   // unser int ms
            null,   // Async. Callback (mehr dazu später)
            null)// Zusätzliche Daten (mehr dazu später)



    while(true)
    {
        Console.Write(".");
        if (ar.AsyncWaitHandle.WaitOne(50,false))
        {
            Console.WriteLine("\nErgebnis ist jetzt verfügbar!");
            break;
        }
    }
    int result = d.EndInvoke(ar);
    Console.WriteLine("\nErgebnis:" + result);
}


Asynchroner Callback

Jeder der das Beispiel oben ausprogrammiert hat wird bemerkt haben das es bei BeginInvoke neben unseren 2 Parametern auch 2 zusätzliche Parameter gibt. Einen vom Typ AsyncCallback einen vom Typ object. AsyncCallback wird vom .NET zu verfügung gestellt und hat diese Signatur

Code:

void CalculationCompleted(IAsyncResult ar)


Hier das passende Beispiel dazu;

Code:

static void CalculationCompleted(IAsyncResult ar)
{
    CalculateDelegate d = ar.AsyncState as CalculateDelegate;
    int result = d.EndInvoke(ar);
    Console.WriteLine("Ergebnis: " + result);
}

static void Main()
{
    CalculateDelegate d = Calculate;
    d.BeginInvoke(1, 1000, CalculationCompleted, null);
   
    /*An dieser Stelle haben wir keine Kontrolle
     * mehr über die Berechnung und auch keine einfache
     * Möglichkeit an das Ergebnis zu kommen!
     * Darum warten wir einfach mal ca 5000 ms (5 sekunden)
     * in einer Schleife
     */

    for (int i = 0; i < 500; i++)
    {
        Console.Write(".");
        Thread.Sleep(50);
    }
}


Wie im Code Kommentar zu lesen haben wir im HauptThread keine Kontrolle mehr über die Berechnung! Für unser Beispiel ist also diese Methode schlecht gewählt. Wir haben einen Vorteil das die Methode Console.WriteLine() Threadsafe ist! Das heisst das sie intern einige Synchronisierungs Methoden hat um sicherzustellen das jeder Thread sie aufrufen kann und den Text in die Konsole schreibt. Würden wir uns in einem Windows Forms programm befinden und wir würden versuchen das Ergebnis in eine Textbox zu schreiben würde das Programm abstürzen. Da Modifikationen nur aus dem Thread passieren dürfen der die Form erstellt und geöffnet hat!
Um das Ergebnis in den Hauptthread zu bekommen muss man Synchronisierungs Code schreiben das werden wir aber erst später behandeln.

Kleiner Experten Tip:
Mit .NET 3.5 gibt es auch die Möglichkeit delegates "inline" zu schreiben. Diese Methodik eignet sich vor allem dazu wenn man delegate nur einmal braucht und dieser sehr kurz ist. In Unserem Beispiel würde das dann so aussehen

Code:

static void Main()
{
    CalculateDelegate d = Calculate;
    d.BeginInvoke(  1,
                    1000, ar =>
                              {
                                  int result = d.EndInvoke(ar);
                                  Console.WriteLine("Ergebnis: " + result);
                              },
                    null);
   
    for (int i = 0; i < 500; i++)
    {
        Console.Write(".");
        Thread.Sleep(50);
    }
}


System.Threading.Thread

Bis jetzt haben wir eine Berechnung im Hintergrund ausgeführt, auf das Ergebnis mehr oder weniger schön gewartet und den Benutzer informiert das wir immer noch berechnen. Der Einzige Vorteil den wir daraus bekommen haben war das wir ..... in die Konsole geschrieben haben während wir berechnet haben.
Ab jetzt werden wir mehrere Threads starten und Dinge gleichzeitig erledigen.
Einen Thread zu erstellen ist einfach.
Code:
Thread t1 = new Thread(ThreadMethode);

ThreadMethode ist ein ThreadStart Delegate.
Die Instanz t1 hat jede Menge Methoden und Properties. Wir könne z.b. Jedem Thread einen Namen geben (erleichtet das Debuggen)
Code:
t1.Name = "Mein Thread"

Auch können wir die Priorität festlegen wie das Betriebssystem die Threads behandeln soll.
Code:
t1.Priority = ThreadPriority.Highest;

Diese Einstellung sagt dem Thread Sheduler wie wichtig der Thread ist uns wird ihm entsprechend der Einstellung mehr oder weniger CPU Zeit gönnen. Ich möchte aber in diesem Tutorial nicht genauer darauf eingehen. Grob gesagt bekommen Threads mit höhere Priorität mehr CPU Zeit als welche mit niedriger Priorität.

Wir schreiben jetzt mal ein kleines Testprogramm das 2 Threads startet die jeweils bis 10.000 zählen.

Bei diesem Test werden wir auch gleich einen entscheidenden Unterschied zu den asynchronen Aufrufen (die auf der Threadpool Klasse basieren und immer Background Threads sind) feststellen. Wird unser Hauptprogramm beendet werden die laufenden Threads NICHT beendet !!! Würde einer unser Threads eine Endlos Schleife haben würde sich das Programm nicht einfach beenden.
Weiters werden Threads nicht automatisch gestartet! Man muss die Start() Methode aufrufen. Man kann Threads aber auch Pausieren und Abbrechen. Und noch einiges mehr... Aber im Moment geben wir uns mal damit zufrieden einen bzw Zwei Threads zu erstellen und diese zu Starten.

Code:

static void Test()
{
    for (int i = 0; i < 10000; i++)
    {
        Console.WriteLine("{0}, {1}", Thread.CurrentThread.Name, i);
    }
}
static void Main()
{
    Thread t1 = new Thread(Test);
    t1.Name = "Erster Thread";           

    Thread t2 = new Thread(Test);
    t2.Name = "Zweiter Thread";
    t1.Priority = ThreadPriority.Highest;
    t2.Priority = ThreadPriority.Lowest;

    t1.Start();
    t2.Start();
}


Sieht ja schon sehr hübsch aus Smile Aber was bringt uns das ? Nicht wirklich viel. Bei fast allen Methoden wollen wir Parameter übergeben. Würden wir unsere Test Methode so umschreiben
Code:
static void Test(string text)

Würde Visual Studio bzw. der Compiler sofort beim Thread erstellen meckern das Die Signatur nicht passt. Ändern wir das ganze aber auf
Code:
static void Test(object text)


Doch wie kommen jetzt die Daten in den Thread ? Alle die das Beispiel brav abgetippt haben wissen das vielleicht als sie t1.Start( getippt haben. Hier kann man ein object übergeben! Was aber wenn man viele Parameter braucht ? Das sollte jeder Programmierer wissen. Wenn alle Parameter den gleichen Typ haben kann man das ellegant über ein Array lösen. Wenn nicht dann schreibt man sich einfach eine Struct oder eine Klasse die die Daten hält und übergibt diese. Etwas unschön aber durchaus ab und zu brauchbar ist alle Daten in ein object[] zu packen und übergeben.
Ein explizites Beispiel spare ich hier mal das sollte jeder selber hinbekommen Smile

Background Threads

Wie schon in der Einleitung zu System.Threading.Threads erwähnt laufen die Threads einfach weiter auch wenn das Hauptprogramm sich beendet hat. Grund dafür ist das per Default alle Threads Vordergrund Threads sind! Solange ein so ein Thread existiert "lebt" das Programm weiter. Wie der Name der überschrift schon verrät gibt es auch Background Threads!

Der Unterschied bei der Erstellung ist ein einfaches bool Flag. Namens IsBackground. Setzt man dieses auf True wird der Thread grausam beendet wenn der letzte Vordergrund Thread beendet wird.
Grausam beendet muss man hier wörtlich nehmen ! Man kann nicht sagen an welcher Code Stelle der Thread beendet wird!. jeder Thread wirft eine ThreadAbortException, befindet sich die Codestelle gerade in einem try/catch/finally block wird catch und finally aufgerufen! Es ist empfehlenswert die ThreadAbortException explizit abzufangen und darauf zu reagieren.

Zum testen müssen wir nur in unserem beispiel diese 2 Zeilen vor dem Starten der Threads einfügen:

Code:

t1.IsBackground = true;
t2.IsBackground = true;


Das Ergebnis ist dann sehr schön ersichtlich wenn wir das Programm starten (Am besten nicht vom Visual Studio sondern direkt in einer Command Shell aus sonst sieht man nicht viel)

Thread Kontrolle
Es gibt ein paar Methoden um einen Thread zu kontrollieren. Zwei davon haben wir schon kennen gelernt: Start() und Sleep(). Was genau sie tun wissen wir ja mittlerweile. Eine weitere Methode zur Kontrolle ist Thread. Abort() welche genau das oben beschriebene tut. Den Thread Abbrechen in dem es eine ThreadAbortException wirft. Man kann diesen Abbruch aber auch mit Thread.ResetAbort() aufhalten aber solche Szenarien sind eher selten und nicht so einfach handzuhaben. bei . Eine weitere sehr oft gebrauchte Methode ist Join(). Mit Join kann man warten bis ein Thread beendet ist. Ich verwende diese Methode sehr häufig bei den Thread Synchronisations Beispielen weiter unten in diesem Artikel.

Bei NET 1.0 gab es noch Thread.Suspend und Thread.Resume. Diese sollte man aber nicht verwenden (man wird auch mit einer Compiler Warning darauf hingewiesen). Um einen Thread zu pausieren sollte man sich die Kapitel über ThreadLocking und Synchronisierung anschauen.

Threadpool
Das Erstellen von Threads braucht Zeit! Hat man nur sehr kurze Berechnungen dann lohnt es sich manchmal gar nicht Threads zu verwenden. Hat man sehr viele kurze Berechnungen und man Erstellt einen Thread pro Berechnung dann dauert das Erstellen der Threads länger als das Berechnen. Außerdem nutzen 1000 Berechnungs-Threads nichts wenn der Rechner nur 4 CPU kerne hat.
Mal angenommen wir haben einen Datenpool mit 1000 Datensätzen. Für jeden Datensatz soll eine Berechnung ausgeführt werden die die einen CPU Kern für 50 ms auf voll auslastet.

Nutzen wir einen CPU Kern wird die Berechnung ziemlich genau 50000 ms also 50 Sekunden dauern. Würden wir jetzt 1000 Threads erstellen und die Berechnung auf alle 4 CPU Kerne aufteilen so würde es nicht 50 ms dauern und auch nicht 12500 ms (= 50.000/4) Sondern vermutlich so zwischen 30.000 und 40.000. Da die Erstellung der 1000 Threads alleine schon einige Sekunden dauern kann (Sehr Betriebssystem und Rechner abhängig). Wir wissen aber das wir wenn wir 4 Berechnungen gleichzeitig machen lasten wir den Rechner zu 100% aus (also jeden Kern 100%). So würden wir keine Unnötigen Threads erstellen und nutzen die gegebenen Ressourcen optimal.
Bei unserer Lösung würden wir dann nur etwas mehr als 12500ms für die gesamte Berechnung brauchen. Wenn wir alles das wir derzeit über Threads wissen wird das eine große Herausforderung man müsste zählen wie viele Threads gerade laufen. Müsste wissen wie man bestehende Threads einfach wiederverwendet kann etc... Microsoft war aber so freundlich und hat uns die ganze Arbeit abgenommen und uns eine Klasse Namens ThreadPool geschenkt!

Threadpool ist eine statische Klasse. Das heisst alle Änderungen die man an den Min und Max Threads macht beziehen sich auf alle Threadpools der Applikation. Per Default sind 1000 Managed und 1000 Unmanaged (für I/O) eingestellt als Maximum. Das heisst aber nicht das 1000 Threads erstellt werden!!! Die Threadpool Klasse ist so intelligent das er selber sehr gut bestimmt wieviele Threads er erstellt. Wenn man an den Einstellungen nichts ändert werden normalerweise so viele Threads wie CPU Kerne erstellt. Um den Threadpool zu zwingen mehr Threads zu erstellen muss man das Minium Limit anheben! In unserem Beispiel werden wir Min und Max auf 4 Threads Stellen und danach könnt ihr mit den Werten Experimentieren. Wir verändern immer die Managed Variante da wir mit QueueUserWorkItem arbeiten und nicht mit UnsafeQueueUserWorkItem. Ich möchte auf UnsafeQueueUserWorkItem nicht weiter eingehen. Der Unterschied zu QueueUserWorkItem ist das die Unsafe Variante einem erlaubt den Callstack sowie den Security Context des Worker Threads zu verlassen. Sowie unmanaged Code auszuführen...
So aber jetzt zum Beispiel:

Code:

static void Main()
   {
     // Threadpool Einstellen
     ThreadPool.SetMinThreads(4, 4);
     ThreadPool.SetMaxThreads(4, 4);

     // Threadpool Einstellungen Abfragen
     int nWorkerThreads;
     int nCompletionPortThreads;
     ThreadPool.GetMaxThreads(out nWorkerThreads, out nCompletionPortThreads);
     /* Weiters kann man auch das abfragen
     ThreadPool.GetAvailableThreads(out nWorkerThreads, out nCompletionPortThreads)
     ThreadPool.GetMinThreads(out nWorkerThreads, out nCompletionPortThreads)
     */

     Console.WriteLine("Max worker threads: {0}, I/O completion threads: {1}", nWorkerThreads, nCompletionPortThreads);
     

      // 1000 Berechnungen starten
     for (int i = 0; i < 1000; i++)
        ThreadPool.QueueUserWorkItem(Berechnung);


     // Wir warten hier einfach bis alle Threads fertig sind.
     // Wie man das machen besser machen kann gibts im den nächsten Teilen des Tutorials
      Console.ReadKey();
  }


  static void Berechnung(object state)
  {
      double x;
      Random r = new Random();
      DateTime stopat = DateTime.Now.AddMilliseconds(50);
      int nWorkerThreads;
      int nCompletionPortThreads;
      ThreadPool.GetAvailableThreads(out nWorkerThreads, out nCompletionPortThreads);
      Console.WriteLine("Starte Berechnung ThreadID:" + Thread.CurrentThread.ManagedThreadId);
     
      while (DateTime.Now < stopat)
      {
          //Irgendwas berechnen Smile
          x = Math.Sin(Math.PI)*Math.Cosh(Math.PI/r.NextDouble());
          if (x == 0.1337)
          {
              Console.WriteLine("lol so ein Zufall");
          }
      }
      Console.WriteLine("ENDE Berechnung ThreadID:" + Thread.CurrentThread.ManagedThreadId);

  }


So nun wissen wir einiges wie man Threads erstellt wie man sie Startet und wie man sehr elegant Berechnungen auf mehrere CPUs verteilt.

In den nächsten Abschnitten werden wir uns um die Lösung der Probleme kümmern die wir uns gerade eingefangen haben. Nämlich die Probleme die entstehen wenn mehrere Threads mit den gleichen Daten arbeiten sollen. Und wie man Wartet bis ein Thread fertig ist.


ThreadLocking und Synchronisation
Das Beste das man machen kann ist Thread Synchronisation zu vermeiden, indem man keine Daten zwischen Threads austauschen muss. Da sich das aber leider nicht immer vermeiden lässt hier ein kleiner Überblick über die Möglichkeiten die sich bieten. Synchronisation ist notwendig um sicherzustellen das nur ein Thread zur selben Zeit Daten verändert. Die Technik dabei nennt sich Threadlocking. Kurz gesagt ein Thread sperrt für alle anderen den Zugriff. Funktioniert in der Theorie sehr einfach doch es kommt sehr schnell zu Problemen wenn entweder zu viel gelockt wird, oder wenn auch nur ein Zugriff nicht gelockt wird. Der schlimmste Fall der eintreten kann nennt sich Deadlock. Ein Deadlock passiert wenn zwei oder mehr Threads gegenseitig einen Lock halten und darauf warten das der andere Thread seinen Lock frei gibt.

Möglichkeiten
In C# gibt es einige Methoden Threads zu locken. Ich werde versuchen hier einige aufzuzählen und näher zu beschreiben.


  • lock Statement
  • Interlocked Klasse
  • Monitor Klasse
  • Wait handles
  • ---Mutek
  • ---Semaphore
  • ---System.Threading.Events
  • ReadWriterLockSlim Klasse (ab .net 3.0)


Das lock Statement
Die wohl einfachste Methode um Threads zu synchronisieren ist das C# keyword lock.
Der Code dafür sieht so aus
Code:

Object obj = new Object()
.......
   lock (obj)
   {
            //Synchronisierte Anweisungen
       }
Bzw. um static member zu locken:
   lock (typeof(MyStaticClass))
       {
               //Synchronisierte Anweisungen
       }


Was genau passiert:

Wenn ein Thread auf diese Codestelle trifft und kein anderer Thread gerade den Code zwischen den Klammern ausführt "reserviert" sich dieser Thread jetzt den lock. Will ein 2. Thread den Code ausführen während der 1. ihn noch ausführt dann muss dieser warten bis der erste fertig ist. Sobald der 1. fertig ist gibt dieser den lock frei und der 2. "reserviert" ihn sich.
Das reservieren und freigeben des locks braucht Zeit! genauso das warten bis der lock frei ist daher sollte man sich vorher überlegen ob man wirklich einen lock braucht oder nicht.
Beispiel:
Ich möchte anhand eines kleinen Beispiels die Notwendigkeit eines ThreadLockings zeigen.

Hier zuerst der Code OHNE lock:
Code:

static void Main(string[] args)
        {
            int numOfThreads = 10;  // Anzahl der Threads
            Data data = new Data(); // Daten Klasse
            Thread[] threads = new Thread[numOfThreads]; // Threads

            //Threads starten
            for (int i = 0; i < numOfThreads; i++)
            {
                threads[i] = new Thread(new DemoTask(data).DoIt);
                threads[i].Start();
            }
           
            //Warten bis alle Threads fertig sind:
            for (int i = 0; i < numOfThreads; i++)
            {
                threads[i].Join();
            }

            // Wir haben 10 Threads gestartet die alle 25.000 mal 1 zu data.Num hinzuzählen.
            // Theoretisch müsste die Summe da 250.000 sein aber dem ist nicht so!
            Console.WriteLine("Summe: " + data.Num);

            // In meinem Fall steht da jetzt "Summe: 205289"

            Console.ReadKey();
        }

        public class Data
        {
            public int Num { get; set;}
        }
        public class DemoTask
        {
            private Data d;
            public DemoTask(Data data)
            {
                this.d = data;
            }

            public void DoIt()
            {
                for (int i = 0; i < 25000; i++)
                    d.Num += 1;
            }
        }


Wie man dem Kommentar entnehmen kann rechnet dieser Code falsch! Der Grund dafür ist das d.Num mehrfach gleichzeitig inkrementiert wird und deshalb einfach Werte vergessen werden.
Einige werden sich jetzt denken. Das Threadlocking hier einzubauen ist ja einfach ich mache bei der Klasse Data einfach ein lock beim Getter und Setter und shon ist es Threadsafe...
Die Klasse würde dann in etwa so aussehen:

Code:

public class Data
        {
            private Object threadLock = new Object();
            private int num;
            public int Num
            {
                get
                {
                    lock (threadLock)
                    {
                        return num;
                    }
                }
                set
                {
                    lock (threadLock)
                    {
                        num = value;
                    }
                }
            }
        }


Sieht auf den Ersten Blick richtig aus aber führt man den Code dann aus kommen noch immer nicht die gewünschten 250.000 heraus. Warum ?
Dazu muss man verstehen wie diese Zeile funktioniert:
d.Num += 1;
mal angenommen d.Num ist 41:
Am Papier würden wir einfach sagen 41+1 = 42 also ist d.Num nach dieser Operation dann 42

Der Prozessor macht das ein bisschen anders.
1. er holt sich den Wert von d.Num aus dem Speicher (schreibt diesen in ein Register)
2. er schreibt den Wert 1 in ein anderes Register
3. er Summiert die 2 Werte
4. er schreibt den Summierten Wert wieder an die "alte" Stelle
Würde das Programm ohne Threads laufen würde das super funktionieren da alle Befehle nacheinander ablaufen. In unserem Fall ist das aber nicht so!
Wenn wir uns die geänderte Data Klasse ansehen und mit dem finden wir vielleicht das Problem...
Wir locken den Lesenden Zugriff und den Schreibenden Zugriff. Verbinden wir jetzt mal unsere Klassen Änderung mit dem Wissen über den Prozessor Ablauf. Dann ergibt sich dieses Szenario:
1. lock im Getter damit wir exklusiven Zugriff haben
2. er holt sich den Wert von d.Num aus dem Speicher (schreibt diesen in ein Register)
3. lock im Getter freigeben
4. er schreibt den Wert 1 in ein anderes Register
5. er Summiert die 2 Werte
6. lock im Setter damit wir exklusiven Zugriff haben
7. er schreibt den Summierten Wert wieder an die "alte" Stelle
8. lock im Setter freigeben

Na den Fehler gefunden ?
War doch eigentlich ganz offensichtlich! Immer noch können sich mehrere Threads den gleichen Wert aus dem Speicher holen. Nämlich während der "Prozessor Steps" 4 und 5.
Damit unser Code also richtig Funktioniert muss der Prozessor Ablauf so aussehen:

1. lock
2. er holt sich den Wert von d.Num aus dem Speicher (schreibt diesen in ein Register)
3. er schreibt den Wert 1 in ein anderes Register
4. er Summiert die 2 Werte
5. er schreibt den Summierten Wert wieder an die "alte" Stelle
6. lock freigeben

Mit diesem Wissen bauen wir mal wieder unseren Code um:
die Data Klasse ändern wie zurück auf

Code:

        public class Data
        {
            public int Num { get; set;}
        }

Und die DemoTask ändern wir auf:

Code:

public class DemoTask
        {
           
            private Data d;
            public DemoTask(Data data)
            {
                this.d = data;
            }
            public void DoIt()
            {
                for (int i = 0; i < 25000; i++)
                {
                    lock (d) // Dadurch wird die Instanz von Data gelockt
                    {
                        d.Num += 1;
                    }
                }
            }
        }


Will man das Locking aber in der Data Klasse machen so geht das auch.
Das ganze Sieht dann so aus:
Code:

public class Data
        {
            public int Num { get; private set; }

            private Object threadLock = new Object();
            public int Increment(int val)
            {
                lock (threadLock)
                {
                    return ++Num;
                }

            }
        }
        public class DemoTask
        {
            private Data d;
            public DemoTask(Data data)
            {
                this.d = data;
            }
            public void DoIt()
            {
                for (int i = 0; i < 25000; i++)
                {
                    d.Increment(1);
                }
            }
        }


So viel Code nur um einen Integer zu inkrementieren.... Das haben sich die microsoft Entwickler wohl auch gedacht und uns eine Klasse geschenkt die solche einfachen Operationen Threadsafe macht die Klasse Interlocked

Interlocked Klasse


Interlocked ist eine statische Klasse für einfache Threadsafe-Operationen.
Hier die Methoden im Überblick:

  • Increment(val) --> Inkrementiert einen 32 Bit (int) bzw 64 Bit (long) Integer und liefert diesen zurück
  • Decrement(val) --> Dekrementiert den Wert und liefert ihn zurück (Man kann auch Inkrement mit negativen Vorzeichen nutzen das macht exakt das gleiche)
  • Exchange(location,value) bzw Exchange<T>( location,value) --> Setzt location auf den Wert value
  • ComparedExchange(location,value, comparand) --> location und comparand werden verglichen wenn beide Gleich sind wird der location auf den Wert value gesetzt und der orignal Wert von location wird zurückgegeben.
  • Add(v1,v2) --> Integer (32 bzw 64bit) Addition von v1 und v2. Die Summe wird zurückgegeben.
  • Read() --> Einen 64Bit Wert auf einen 32 Bit Betriebssystem aus dem Speicher lesen. Auf einem 64 Bit Betriebssystem (und 64 Bit compiliert) ist das nicht notwendig!

Alle Operationen der Interlocked Klasse sind optimiert und um ein vielfaches schneller als alle anderen Thread-Locking Methoden. Aber wie man an den Methoden sieht nur für sehr einfache Operationen zu gebrauchen.
Hier ein paar Beispiele lock vs. Interlocked:

Code:

lock(this)
{
   if (someValue == null)
      someValue = newValue;
}
//Ist das Gleiche wie nur das Interlocked schneller ist Smile
Interlocked.CompareExchange<TestClass>(ref someValue, newValue, null);


Oder wie unser Beispiel zuvor:
Code:

private int state;
public int State
{
    get
    {
        lock (this)
        {
            return ++state;
        }
    }
}
//Ist das Gleiche wie nur das Interlocked schneller ist Smile
public int State
{
    get
    {
        return Interlocked.Increment(ref state);
    }
}

Mehr gibt es eigentlich nicht zu sagen.

Monitor Klasse
Alle die sich ein bisschen mit der Sprache C# auskennen wissen das Keywords nur einfache Schreibweisen für Codeblöcke sind. Das Keyword lock ist da keine Ausnahme.
Hinter :
Code:
lock (obj)
{
    //do Something
}

versteckt sich dieser Code:
Code:

Monitor.Enter(obj);
try
{
    //do Something
}
finally
{
    Monitor.Exit(obj);
}


Die Monitor Klasse kann aber einiges mehr als das lock Keyword. Der größte Vorteil sind die Timeouts die man nutzen kann.
Man kann Bestimmen wie lange auf den Lock gewartet werden soll. Das ist besonders wichtig wenn es um Zeitkritische Applikationen geht. Zum Beispiel Spieleentwicklung!
Der Code dafür ist recht einfach und benötigt meiner meinung nach keiner weiteren Erklärung:
Code:

if (Monitor.TryEnter(obj,TimeSpan.FromMilliseconds(500)))
{
    try
    {
        //do Something
    }
   finally
    {
        Monitor.Exit(obj);
    }
}
else
{
    // Lock nach 500 ms nicht bekommen
    // do Something different ....
}


Wait Handle
WaitHandle ist eine abstrakte Klasse die man verwenden kann um auf Signale zu warten. Die Ableitungen die wir uns genauer anschauen werden sind Mutex, Semaphore und Event. Zuerst möchte ich zusätzlich zum einen Beispiel am Begin des Artikels noch ein weiteres Waithandler Beispiel bringen.

Code:

public class AsyncDemo
{
// Die Methode die Asynchron aufgerufen werden soll
public string TestMethod(int callDuration, out int threadId)
{
    Console.WriteLine("Start.");
    Thread.Sleep(callDuration);
    threadId = Thread.CurrentThread.ManagedThreadId;
    return String.Format("Laufzeit: {0}.", callDuration.ToString());
}
}


// Der Delegate muss die selbe Signatur haben wie die Methode  die Aufgerufen wird!!!       
public delegate string AsyncMethodCaller(int callDuration, out int threadId);

static void Main(string[] args)
{

int threadId;
AsyncDemo ad = new AsyncDemo();
// Erstellen des Delegates der Asynchronaufgerufen werden soll
AsyncMethodCaller caller = new AsyncMethodCaller(ad.TestMethod);

// Inizialisieren (duration,out threadid, null (Callback), null (object das man an den callback schleifen will)
IAsyncResult result = caller.BeginInvoke(3000,
    out threadId, null, null);

Thread.Sleep(0);
Console.WriteLine("Main thread {0} does some work.",Thread.CurrentThread.ManagedThreadId);

// Warten bis der Asynchrone Aufruf fertig ist.
result.AsyncWaitHandle.WaitOne();

// Ergebnis des Asychronen Calls abrufen
string returnValue = caller.EndInvoke(out threadId, result);

// Den Wait handle wieder schließen! (Sollte man nie vergessen!!!)
result.AsyncWaitHandle.Close();

Console.WriteLine("Der Asynchrone Thread {0}, Liefert das Ergebnis: \"{1}\".",
    threadId, returnValue);
}


Einige werden sich vermutlich denken "Das kommt mir irgendwie bekannt vor" (nicht nur aus diesem Tutorial) wenn sie das Beispiel durchlesen.... Ja das stimmt Smile Wir arbeiten oft mit Wait Handles aber merken es nicht. Bzw. wussten nicht genau was dahinter steckt. Wenn man mit Webservices arbeitet, oder I/O Operationen asynchron durchführt und noch an vielen anderen Stellen...
Bevor wir uns mit den speziellen Wait Handler die ich euch zeigen will beschäftigen schulde ich euch noch die wichtigen Methoden der Wait Handler Klasse.


  • WaitOne(...) --> (Instance Methode) Blockiert den aktuellen thread und wartet auf ein Signal. Es gibt auch Signaturen die Timeouts vorsehen damit nach einer gewissen Zeit abgebrochen wird.
  • WaitHandle.WaitAll(...) --> (Static Methode) Es wird auf mehrere WaitHandles gewartet und erst nachdem ALLE Handles ein Signal gesendet haben (also fertig sind)
  • WaitHandle.WaitAny(...) --> (Static Methode) Funktioniert wie WaitAll mit dem kleinen aber feinen Unterschied, dass nach dem ersten Signal das Programm weiterläuft.


Mutex
Das Wort Mutex (mutual exclusion) heisst übersetzt wechselseitiger Ausschluß. Mutex funktioniert ähnlich wie die Monitor Klasse mit einem großen Unterschied. Mutex ist nicht auf den Prozess gebunden und erlaubt Systemweites locking über mehrere Prozesse.

Code:

bool created;
    Mutex mutex = new Mutex(false,"MeinMutex",out created);

    if (mutex.WaitOne(100))
    {
        try
        {
            // Synchronisiert

        }finally
        {
            mutex.ReleaseMutex();
        }


    }
    else
    {
        Console.WriteLine("Problem beim Warten... (Timeout)");
    }


Der String "MeinMutex" dient zur Identifizierung. Jeder Prozess der diesen String verwendet wird darauf reagieren also sollte man Vorsichtig bei der Wahl des Namens sein.
Ein Mutex kann auch ohne diesen Namen erstellt werden und ist dann local auf den Prozess beschränkt.
Da Mutex von WaitHandle abgeleitet ist gelten die gleichen Regeln und verfahren wie zuvor beschrieben.
Dieses Beispiel hat zwar nichts mit Thread Synchronisation im eigentlichen Sinn zu tun aber es passt sehr gut zum Mutex:

Verhindern das ein Programm 2x gestartet wird
Code:

static void Main(string[] args)
{

    bool createdNew;
    Mutex mutex = new Mutex(false,
                System.Reflection.Assembly.GetExecutingAssembly().FullName,
                out createdNew);
    if (!createdNew)
    {
        Console.WriteLine("Dieses Programm kann nur einmal gestartet werden!");
        return;
    }
    //......
}



Semaphore
So jetzt wird es ein bisschen komplizierter. Zuerst das einfache... Der Semaphore ist von WaitHandler abgeleitet arbeitet also genauso wie der Mutex. ABER....

Ein Semaphore kann von mehreren Threads gleichzeitig verwendet (das heisst gelockt) werden.
Wenn jetzt immer noch ??? über eurem Kopf steht hier eine vielleicht einfachere Erklärung :
Mal angenommen ihr habt einen PC mit 2 Seriellen Ports (Oder sonst irgendwelche Schnittstellen). Das heißt ihr könnt mit 2 Ports gleichzeitig arbeiten. Wenn aber 10 Threads gleichzeitig wollen müssen 8 warten.
Das Beispiel sieht in etwa so aus:

Code:

static void ThreadMain(object o) // Thread der die Arbeit tut
{
    Semaphore semaphore = o as Semaphore;
    Trace.Assert(semaphore != null, "o muss vom Typ semaphore sein");
    bool isCompleted = false;

    while (!isCompleted)
    {
        if (semaphore.WaitOne(250,false)) // Lock holen
        {
            try
            {
                Console.WriteLine("Thread {0} lockt the semaphore",
                    Thread.CurrentThread.ManagedThreadId);
                Thread.Sleep(1500); //Irgendwas tun Smile

            }finally
            {
                semaphore.Release(); // Freigeben
                  Console.WriteLine("Thread {0} gibt semaphore frei!",
                    Thread.CurrentThread.ManagedThreadId);
                  isCompleted = true;

            }
        }
        else // kein Lock bekommen. Nochmal warten
        {
              Console.WriteLine("Timeout für Thread {0}! Nochmal warten...",
                    Thread.CurrentThread.ManagedThreadId);
        }
    }

}

public static void Main()
{

    int numOfThreads = 5;  // Anzahl der Threads
    int semaphoreCount = 2;
    Thread[] threads = new Thread[numOfThreads];

    bool createdNew; // Werde ich in diesem Beispiel einfach ignorieren Smile

    //Semaphore Erstellen!
    // Der Erste Parameter gibt an mit wievielen Threads der Semaphore erstellt werden soll.
    // Der 2. die Maximalanzahl an Threads an die gleichzeitig locken können.
    Semaphore semaphore = new Semaphore(semaphoreCount, semaphoreCount);

    //Das wäre die Version die auch Prozessübergreifend funktioniert!
    //(createdNew muss aber dann auch abgerufen werden
    /*Semaphore semaphore = new Semaphore(semaphoreCount,
                            semaphoreCount,
                            "MeinSemaphore",
                            out createdNew);
     *
     * */


    //Threads starten
    for (int i = 0; i < numOfThreads; i++)
    {
        threads[i] = new Thread(ThreadMain);
        threads[i].Start(semaphore);
    }

    //Warten bis alle Threads fertig sind:
    for (int i = 0; i < numOfThreads; i++)
    {
        threads[i].Join();
    }


    /* DAS Ergebnis sieht in etwa so aus
     *
Thread 11 lockt the semaphore
Thread 12 lockt the semaphore
Timeout für Thread 13! Nochmal warten...
Timeout für Thread 14! Nochmal warten...
Timeout für Thread 15! Nochmal warten...
Timeout für Thread 13! Nochmal warten...
Timeout für Thread 14! Nochmal warten...
Timeout für Thread 15! Nochmal warten...
Timeout für Thread 13! Nochmal warten...
Timeout für Thread 14! Nochmal warten...
Timeout für Thread 15! Nochmal warten...
Timeout für Thread 13! Nochmal warten...
Timeout für Thread 14! Nochmal warten...
Timeout für Thread 15! Nochmal warten...
Timeout für Thread 13! Nochmal warten...
Timeout für Thread 14! Nochmal warten...
Timeout für Thread 15! Nochmal warten...
Timeout für Thread 13! Nochmal warten...
Timeout für Thread 14! Nochmal warten...
Thread 11 gibt semaphore frei!
Thread 14 lockt the semaphore
Thread 12 gibt semaphore frei!
Thread 13 lockt the semaphore
Timeout für Thread 15! Nochmal warten...
Timeout für Thread 15! Nochmal warten...
Timeout für Thread 15! Nochmal warten...
Timeout für Thread 15! Nochmal warten...
Timeout für Thread 15! Nochmal warten...
Timeout für Thread 15! Nochmal warten...
Thread 14 gibt semaphore frei!
Thread 15 lockt the semaphore
Thread 13 gibt semaphore frei!
Thread 15 gibt semaphore frei!
     *
     */

    Console.ReadKey();

}


Event

So das wichtigste zuerst:
Wir reden hier von der Klasse System.Threading.Event und nicht vom Keyword event

Wie schon erwähnt ist auch die Event Klasse von WaitHandle abgeleitet, ist somit auf Wunsch Systemweit verfügbar, Im Hintergrund wird mit System events des Betriebssystems gearbeitet die ich jetzt nicht genauer erklären will. Wir beschränken uns auf die .NET Implementierung. Die Klassen dafür heißen

  • ManualResetEvent
  • AutoResetEvent


Der Unterscheid zwischen ManualResetEvent und AutoResetEvent ist das das ManualResetEvent nicht Automatisch resettet wird wenn ein WaitOne fertig ist.
Man kann sich das in etwa so vorstellen:
Eingang in eine Diskothek: 100 Leute stehen vor der Tür
AutoResetEvent: Kunde kommt zur Tür - Türsteher Öffnet Tür -- Der Gast geht rein --> Türsteher schließt Tür --> Nächster Kunde kommt zur Tür ...
ManualResetEvent: : Kunde kommt zur Tür - Türsteher Öffnet Tür -- x Gäste gehen rein solange die Tür offen ist --> Türsteher schließt Tür --> Nächster Kunde kommt zur Tür ...

In den Meisten Fällen wird die AutoResetEvent Klasse verwendet. Daher werde ich auch diesbezüglich ein kleines Sample schreiben.
Für einen Konkreten Fall in der Spiele Entwicklung habe ich auch ein kleines Sample mit ManualResetEvent hier im Forum.

Code:

public struct Data //Daten die für die Berechnung benötigt werden
{
  public int X;
  public int Y;

  public Data(int x, int y)
  {
     X = x;
     Y = y;
  }
}

public class ThreadTask // Berechnungs Thread
{
  private AutoResetEvent autoEvent; // Unser AutoResetEvent

  public int Result { get; private set; }

  public ThreadTask(AutoResetEvent ev)
  {
     autoEvent = ev;
  }

  public void Calculation(object obj)
  {
     Data data = (Data)obj;
     Console.WriteLine("Thread {0} startet Berechnung",
        Thread.CurrentThread.ManagedThreadId);

     // Berechnung starten

     // Ein bisschen Arbeit simulieren
     Thread.Sleep(new Random().Next(3000));
     Result = data.X + data.Y;

      //Berechnung Beendet
     
     Console.WriteLine("Thread {0} Berechnung abgeschlossen",
        Thread.CurrentThread.ManagedThreadId);
     autoEvent.Set(); // Signalisieren das wir fertig sind !

  }
}

class Program
{
  static void Main()
  {
     int taskCount = 4;

     AutoResetEvent[] autoEvents = new AutoResetEvent[taskCount];
     ThreadTask[] tasks = new ThreadTask[taskCount];

     for (int i = 0; i < taskCount; i++)
     {
        autoEvents[i] = new AutoResetEvent(false); //mit false initialisieren
        tasks[i] = new ThreadTask(autoEvents[i]);

        if (!ThreadPool.QueueUserWorkItem(
                tasks[i].Calculation,
                new Data(i + 1, i + 3)))
        {
           Console.WriteLine("Thread konnte nicht gestartet werden!");
        }
     }

     for (int i = 0; i < taskCount; i++)
     {
        int index = WaitHandle.WaitAny(autoEvents); //Auf einen Thread Warten
        if (index == WaitHandle.WaitTimeout)
           Console.WriteLine("Timeout!!");
        else
        {
           Console.WriteLine("Ergebnis für ID {0}, = {1}",
               index,
               tasks[index].Result);
        }
     }
      Console.ReadKey();
      /*Ergebnis
       *
Thread 6 startet Berechnung
Thread 11 startet Berechnung
Thread 10 startet Berechnung
Thread 12 startet Berechnung
Thread 12 Berechnung abgeschlossen
Thread 10 Berechnung abgeschlossen
Thread 6 Berechnung abgeschlossen
Thread 11 Berechnung abgeschlossen
Ergebnis für ID 3, = 10
Ergebnis für ID 0, = 4
Ergebnis für ID 1, = 6
Ergebnis für ID 2, = 8
       */

  }

}


ReaderWriterLockSlim

Mein persönlicher Liebling wenn es um Locking geht. ReaderWriterLock gab es schon mit .NET 1.0 dieser wurde aber mit .NET 3.0 komplett neu geschrieben, in ReaderWriterLockSlim umbenannt und ist jetzt weniger anfällig auf Deadlocks und um einiges performanter.
Der große Vorteil ist das man einen WriterLock aber mehrere Reader Locks haben kann und daher werden weniger Aufrufe Blockiert. Solange kein Thread Schreiben will können alle Lesen! Sobald einer zu schreiben beginnt werden wie üblich alle anderen blockiert, wenn das WriteLock wieder aufgehoben ist können wieder alle Lesen,.....
Nebenbei liefert die Klasse auch einige StatusInformationen wie Anzahl der aktuellen ReadCounts, Anzahl der blockierten Threads usw...
Die Methoden im Detail:

  • EnterReaderLock ()--> Fordert einen LeseZugriff an. Ist derzeit niemand am Schreiben wird der Zugriff sofort erteilt. Ansonsten wird gewartet bis kein WriterLock gehalten wird
  • TryEnterReaderLock(int timeout) --> Wie EnterReaderLock nur das man ein Timeout definieren kann
  • ExitReaderLock() --> Verlässt den ReaderLock
  • EnterWriterLock ()--> Fordert einen Schreibenden Zugriff an. Es wird gewartet bis kein anderer Liest oder Schreibt danach hat man exklusiven Zugriff.
  • TryEnterWriterLock (int timeout) --> Wie EnterWriterLock nur das man ein Timeout definieren kann
  • ExitWriterLock () --> Verlässt den ReaderLock
  • EnterUpgradeableReadLock () --> Wenn man nach dem Lesen (eventuell) schreibenden Zugriff braucht sollte man diese Methode verwenden um einen ReadLock zu bekommen. Muss man dann schreiben kann man innerhalb dieses UpgradeAbleWriteLock einen WriteLock anfordern.
  • TryEnterUpgradeAbleWriteLock(int timeout) --> ....
  • ExitUpgradeAbleWriteLock() --> Selbsterklärend Smile


Code:

private static List<int> items = new List<int>{ 0, 1, 2, 3, 4, 5};
  private static ReaderWriterLockSlim rwl =
      new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);

  static void ReaderMethod(object reader)
  {
     try
     {
        rwl.EnterReadLock();

        for (int i = 0; i < items.Count; i++)
        {
           Console.WriteLine("reader {0}, loop: {1}, item: {2}",
               reader,
               i,
               items[i]);
           Thread.Sleep(40);
        }
     }
     finally
     {
        rwl.ExitReadLock();
     }
  }

  static void WriterMethod(object writer)
  {
     try
     {
        while (!rwl.TryEnterWriteLock(50))
        {
           Console.WriteLine("Writer {0} konnte write "+
            "lock noch nicht bekommen ", writer);
           Console.WriteLine("  Anzahl aktueller reader"+
               " : {0}", rwl.CurrentReadCount);
        }
        Console.WriteLine("Writer {0} hat lock", writer);
        Console.WriteLine("Anzahl aktueller reader "+
            "(sollte sein) : {0}", rwl.CurrentReadCount);
        for (int i = 0; i < items.Count; i++)
        {
           items[i]++;
           Thread.Sleep(50);
        }
        Console.WriteLine("Writer {0} exit...", writer);
     }
     finally
     {
        rwl.ExitWriteLock();
     }
  }


  static void Main()
  {
     new Thread(WriterMethod).Start(1);
     new Thread(ReaderMethod).Start(1);
     new Thread(ReaderMethod).Start(2);
     new Thread(WriterMethod).Start(2);
     new Thread(ReaderMethod).Start(3);
     new Thread(ReaderMethod).Start(4);
     Console.ReadKey();
  }



Schlussworte:

Wie man sieht gibt es unterschiedliche Möglichkeiten seinen Code Threadsafe zu machen und Threads zu Synchronisieren. Manchmal gibt es verschiede Lösungen für ein Problem aber oft nur eine richtige. Dieses kleine Tutorial zeigt nur die Möglichkeiten auf. Vielleicht werde ich zu einem späteren Zeitpunkt ein paar spezielle Anwendungsfälle genauer beschreiben. Bis dahin könnt ihr Euch mit Fragen immer an mich wenden!

Ich gebe keine Garantie auf Richtigkeit. Ich bin auch nur ein Mensch und mache Fehler. Ich hoffe aber das ich den Code und die Beschreibungen halbwegs Fehlerfrei dokumentiert habe. Vollständig ist das Thema auf alle Fälle nicht besonders wenn man .NET 4.0 betrachtet da kommt einiges an Veränderungen (vor allem Vereinfachungen) auf uns zu.

Wichtige Hinweise:
Dieser Artikel enthält mit Sicherheit den Einen oder Anderen Rechtschreib und Grammatik Fehler! Wenn jemand einen Findet und ihn korrigiert wissen will schickt mir eine PM.

Sobald ich mal ein bisschen Zeit habe das ganze etwas "bunter" machen Wink Also die Formatierung anpassen!


Danke für eure Aufmerksamkeit


Zuletzt bearbeitet von Ysas am 25.09.2009, 13:26, insgesamt 9-mal bearbeitet
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden Website dieses Benutzers besuchen MSN Messenger ICQ-Nummer


JeReT
Member


Anmeldedatum: 19.07.2007
Beiträge: 3248
Wohnort: µnchen

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 14:35    Titel:

Wow! was für ein umfangreicher Artikel.
So umfangreich, dass ich ihn noch gar nicht gelesen hab...

Solch langen texte drucke ich mir zum lesen lieber aus... Deshalb meine Frage: hast du das ganze auch als .doc oder .pdf dokument? das sieht ausgedruckt meistens schöner aus... also falls du das sowieso vorher in einem ordentlichen textverarbeitungsprogramm gemacht hast, wäre ich froh um einen downloadlink.

Ansonsten bin ich sehr gespannt aufs lesen Smile
_________________
Idea Shapes
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden Website dieses Benutzers besuchen Yahoo Messenger MSN Messenger ICQ-Nummer


Ysas
Member


Anmeldedatum: 07.05.2009
Beiträge: 265
Wohnort: Österreich

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 14:42    Titel:

Ja gibt es.
Die aktuellste Version werde ich immer in meinem Original verlinken:
Zum Original


Zuletzt bearbeitet von Ysas am 10.06.2009, 07:27, insgesamt einmal bearbeitet
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden Website dieses Benutzers besuchen MSN Messenger ICQ-Nummer


Bethsoftfan
Member


Anmeldedatum: 14.09.2008
Beiträge: 809

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 15:38    Titel:

Hallo!

Super echt lesenswert, auch wenn es etwas viel ist, es lohnt sich! Früher habe ich immer alles mit einzelnen Threads gelöst, das waren dann zu viele Threads und es war asynchron!

Bis auf ein paar Fehlern (Schenk dir mal ne Tüte Kommas! Laughing ) ist es spitze!
_________________
Der Bethsoftfanblog
..-> 3D Grundlagen in der XNA Spieleprogrammierung. <-..
Normalmapping + Parallax Occlusion Mapping
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden Website dieses Benutzers besuchen


xyzagent
Member


Anmeldedatum: 31.05.2008
Beiträge: 110

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 16:42    Titel: Re: (Under Construction) Threads - Lock - Synchronisation

Ysas hat Folgendes geschrieben:
Das "(noch)" bezieht sich auf .NET 4.0 wo wirklich einigen sehr viel einfacher wird
dazu werde ich aber ein eigenes Tutorial veröffentlichen.


Hoffentlich geht das auf der XBOX.

lg Jan
_________________
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden


chrische5
Member


Anmeldedatum: 10.11.2007
Beiträge: 1062

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 17:04    Titel:

Hallo

Ich freue mich sehr den Artikel heute Abend in Ruhe zu lesen...

Danke für den Aufwand.

chrische
_________________
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden


Kevin
Member


Anmeldedatum: 24.10.2008
Beiträge: 882
Wohnort: Karlsruhe

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 17:10    Titel:

Vielen Dank, ich werde mir den Artikel bei Gelegenheit mal
durchlesen, und hoffentlich auch verstehen Wink

MfG
Kevin
_________________
Mein Youtube Kanal
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden


SeeQuark
Member


Anmeldedatum: 22.02.2009
Beiträge: 129
Wohnort: CH

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 17:27    Titel:

Sehr schöner Artikel (auch wenn ich ihn bis jetzt nur punktuell gelesen habe (teilweise mit Strg+F Very Happy )).

Ein paar wenige Kritikpunkte:
  1. Ysas hat Folgendes geschrieben:
    Das Erstellen von Threads braucht Zeit! Hat man nur sehr kurze Berechnungen dann lohnt es sich manchmal gar nicht Threads zu verwenden.
    Das Erstellen von Threads dauert nicht besonders lang und ist sogar resourcenschonend. Dennoch liegt nach ihrer Erzeugung ein unnötiger Thread da, das Verwenden vom ThreadPool ist also die richtige Entscheidung.
    Deine "sehr kurzen Berechnungen" sind allerdings auch sehr gut im Main-Thread aufgehoben (siehe Punkt 3).

  2. Ysas hat Folgendes geschrieben:
    Der Hauptthread ist der Erste der gestartet wird und in der Regel der letzte der beendet wird. Er enthält den Programm Einsprungs Punkt also unsere void Main() Methode.

    Wichtig ist dabei allerdings, dass das Programm ERST DANN beendet wird, wenn alle ihre Threads die Arbeit erledigt haben. Ein Thread kann aber auch automatisch beendet werden, wenn man bei ihm IsBackground auf true setzt. Bei dem ThreadPool ist das der Standard.

  3. Bei "Wozu brauche ich Threads" fehlen mir die negativen Argumente.
    Manchmal ist es mit Threads ja teilweise wirklich langsamer (kurze Berechnung in eigenen Thread auslagern => Parameterübergabe etc. braucht mehr Zeit wie die Berechnung) und vor allem viel komplizierter (Debugging und Coding).
    Threading kann nicht nur positiv sein. Auch wird es ab mehr Threads wie die CPU Kerne hat langsamer, noch mehr in Threads aufzugliedern.

  4. Über die "Gefahren" steht mir noch etwas wenig da. Den "Deadlock" hast du ja nur beiläufig erwähnt. Auch über das Schlüsselwort "volatile" solltest du imho noch ein paar Worte verlieren.

  5. Zwar hat es viele Rechtschreibfehler, aber in dem grossen Artikel gehen diese fast unter.

Alles in allem ein sehr ausführlicher und guter Artikel über das Threading.

mfg
SeeQuark
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden


pdelvo
Member


Anmeldedatum: 21.02.2009
Beiträge: 231

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 17:50    Titel:

Sehr umfangreich! Habe nur kurz drübergeguckt.

Zitat:
Windows Forms Applikationen "zeichnen" sich alle paar Millisekunden neu genauso wie DirectX Spiele oder andere Programme mit einer Graphischen Oberfläche.


Das stimmt nicht so ganz. Die Fenster zeichnen sich nur neu, wenn das Fenster sich neuzeichnen muss. Entweder hat sich etwas geändert oder von Windows kommt ein WM_PAINT. Ein Fenster "Reagiert nicht mehr", wenn Windows Messages gesendet wurden und noch nicht verarbeitet wurden. Aber sie zeichnen sich nicht alle paar Milisekunden neu.

Das Tutorial werde ich heute Abend mal komplett durchlesen.

PS: Ich glaube ich muss auch mal einen Artikel schreben. Denke mir mal ein Thema aus.

Gruß pdelvo
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden


Levitas
Member


Anmeldedatum: 25.08.2008
Beiträge: 417

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 18:00    Titel:

Seeehr umfangreich, und auch total gut beschrieben und erklärt. 'Ne glatte 1 von mir!

Wäre das Tut nur zwei Wochen früher gekommen ...^^

Trotzdem gut, ich habe mein Wissen jetzt erweitert, DANKE =)


Mfg Marc
_________________
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden Website dieses Benutzers besuchen ICQ-Nummer


SeeQuark
Member


Anmeldedatum: 22.02.2009
Beiträge: 129
Wohnort: CH

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 18:00    Titel:

pdelvo hat Folgendes geschrieben:
Entweder hat sich etwas geändert oder von Windows kommt ein WM_PAINT.
Wenn sich etwas geändert hat, muss man ein WM_PAINT erzwingen. WPF übernimmt das selber. Polling-mässiges Zeichnen tut nur das BS selber, aber das tut es auch, wenn die GUI einfriert (das Selbe gilt aber auch für DirectX).

Zu diesem Abschnitt habe ich noch was in meiner Liste vergessen: Selbst wenn der GUI-Thread blockiert ist, kann man ein Neuzeichnen erzwingen (Refresh). Allerdings funktioniert das ab Vista nicht mehr besonders zuverlässig.

mfg
SeeQuark
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden


Kevin
Member


Anmeldedatum: 24.10.2008
Beiträge: 882
Wohnort: Karlsruhe

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 18:01    Titel:

Hi,
vielleicht habe ichs überlesen, aber was genau macht ThreadPool.QueueUserWorkItem?
Ich habe eine kleine Testanwendung geschrieben, hier mal der Code:

Code:
      static void Main(string[] args) {
         Stopwatch sw = Stopwatch.StartNew();

         for (int i = 0; i < 5000; i++) {
            Test(null);
         }
         Console.WriteLine(string.Format("Elapsed: {0} ms", sw.ElapsedMilliseconds));

         sw = Stopwatch.StartNew();

         for (int i = 0; i < 5000; i++) {
            ThreadPool.QueueUserWorkItem(Test);
         }

         Console.WriteLine(string.Format("Elapsed (Threaded): {0} ms", sw.ElapsedMilliseconds));
         Console.ReadLine();
      }

      private static void Test(object state) {
         Random rnd = new Random();
         for (int i = 0; i < 1000; i++) {
            double d = rnd.NextDouble() * Math.Sqrt(rnd.NextDouble()) * Math.Sin(Math.PI / rnd.NextDouble()) - Math.Cos(10.0);
         }
      }




Und ich erhalte als Output:
Elapsed: 1044 ms
Elapsed (Threaded): 9 ms

Ich finde die Werte ein bischen extrem, ist deshalb auch meine Frage was genau das QueueUserWorkItem macht...
ich hätte jetzt auf einen Wert um die 250 ms getippt, da ich Quadcore habe...

MfG
Kevin
_________________
Mein Youtube Kanal
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden


SeeQuark
Member


Anmeldedatum: 22.02.2009
Beiträge: 129
Wohnort: CH

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 18:05    Titel:

@Kevin: Der ThreadPool ist nichts anderes als eine Sammlung aus Threads, die nur auf deine Arbeit warten =)
Du hast die Arbeit also noch nicht abgeschlossen, sonder nur gestartet Smile .

Ysas hat Folgendes geschrieben:

Nutzen wir einen CPU Kern wird die Berechnung ziemlich genau 50000 ms also 50 Sekunden dauern. Würden wir jetzt 1000 Threads erstellen und die Berechnung auf alle 4 CPU Kerne aufteilen so würde es nicht 50 ms dauern und auch nicht 12500 ms (= 50.000/4) Sondern vermutlich so zwischen 30.000 und 40.000. Da die Erstellung der 1000 Threads alleine schon einige Sekunden dauern kann (Sehr Betriebssystem und Rechner abhängig). Wir wissen aber das wir wenn wir 4 Berechnungen gleichzeitig machen lasten wir den Rechner zu 100% aus (also jeden Kern 100%). So würden wir keine Unnötigen Threads erstellen und nutzen die gegebenen Ressourcen optimal.
Bei unserer Lösung würden wir dann nur etwas mehr als 12500ms für die gesamte Berechnung brauchen. Wenn wir alles das wir derzeit über Threads wissen wird das eine große Herausforderung man müsste zählen wie viele Threads gerade laufen. Müsste wissen wie man bestehende Threads einfach wiederverwendet kann etc... Microsoft war aber so freundlich und hat uns die ganze Arbeit abgenommen und uns eine Klasse Namens ThreadPool geschenkt!

mfg
SeeQuark
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden


Kevin
Member


Anmeldedatum: 24.10.2008
Beiträge: 882
Wohnort: Karlsruhe

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 18:09    Titel:

Ähh..stimmt Very Happy
Aber wie kann ich herausfinden wann der ThreadPool mit meinen Methoden fertig ist?
Ich habe den Artikel noch nicht fertig gelesen, falls das also noch irgendwo steht, bitte sagen Wink

MfG
Kevin
_________________
Mein Youtube Kanal
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden


SeeQuark
Member


Anmeldedatum: 22.02.2009
Beiträge: 129
Wohnort: CH

Antworten mit Zitat
BeitragVerfasst am: 09.06.2009, 18:13    Titel:

Das steht auch da Smile
Ysas hat Folgendes geschrieben:
ManualResetEvent: : Kunde kommt zur Tür - Türsteher Öffnet Tür -- x Gäste gehen rein solange die Tür offen ist --> Türsteher schließt Tür --> Nächster Kunde kommt zur Tür ...

In den Meisten Fällen wird die AutoResetEvent Klasse verwendet. Daher werde ich auch diesbezüglich ein kleines Sample schreiben.
Für einen Konkreten Fall in der Spiele Entwicklung habe ich auch ein kleines Sample mit ManualResetEvent hier im Forum.

mfg
SeeQuark
Nach oben
Benutzer-Profile anzeigen Private Nachricht senden


Beiträge der letzten Zeit anzeigen:   
 
Neue Antwort erstellen Alle Zeiten sind GMT + 1 Stunde
Gehe zu Seite 1, 2  Weiter
Seite 1 von 2

 
Du kannst keine Beiträge in dieses Forum schreiben.
Du kannst auf Beiträge in diesem Forum nicht antworten.
Du kannst deine Beiträge in diesem Forum nicht bearbeiten.
Du kannst deine Beiträge in diesem Forum nicht löschen.
Du kannst an Umfragen in diesem Forum nicht mitmachen.


Powered by phpBB © 2001, 2005 phpBB Group
Deutsche Übersetzung von phpBB.de