Skip to content

Latest commit

 

History

History
712 lines (576 loc) · 25 KB

23_Threads.md

File metadata and controls

712 lines (576 loc) · 25 KB

LiaScript

Threads

Parameter Kursinformationen
Veranstaltung: Vorlesung Softwareentwicklung
Teil: 23/27
Semester @config.semester
Hochschule: @config.university
Inhalte: @comment
Link auf den GitHub: https://github.com/TUBAF-IfI-LiaScript/VL_Softwareentwicklung/blob/master/23_Threads.md
Autoren @author


Motivation - Threads

Bisher haben wir rein sequentiell ablaufende Programme entworfen. Welches Problem generiert dieser Ansatz aber, wenn wir in unserer App einen Update-Service integrieren?

BlockedGUI

Grundlagen

Ein Ausführungs-Thread ist die kleinste Sequenz von programmierten Anweisungen, die unabhängig von einem Scheduler verwaltet werden kann, der typischerweise Teil des Betriebssystems ist.

Die Implementierung von Threads und Prozessen unterscheidet sich von Betriebssystem zu Betriebssystem, aber in den meisten Fällen ist ein Thread ein Bestandteil eines Prozesses.

Innerhalb eines Prozesses können mehrere Threads existieren, die gleichzeitig ausgeführt werden und Ressourcen wie Speicher gemeinsam nutzen, während verschiedene Prozesse diese Ressourcen nicht gemeinsam nutzen. Insbesondere teilen sich die Threads eines Prozesses seinen ausführbaren Code und die Werte seiner dynamisch zugewiesenen Variablen und seiner nicht thread-lokalen globalen Variablen zu einem bestimmten Zeitpunkt.

Prozess vs Thread

Auf einem Single-Core Rechner organisiert das Betriebssystem Zeitscheiben (unter Windows üblicherweise 20ms) um Nebenläufigkeit zu simulieren. Eine Multiprozessor-Maschine kann aber auch direkt auf die Rechenkapazität eines weiteren Prozessors ausweichen und eine echte Parallelisierung umsetzen, die allerdings im Beispiel durch den gemeinsamen Zugriff auf die Konsole limitiert ist.

Vorteile von Multi-Threading Applikationen:

  • Ausnutzung der Hardwarefähigkeiten (MultiCore-Systeme)
  • Effizienzsteigerung bei Wartevorgängen
  • Verhinderung eines "Verhungerns" der Anwendung

Erfassung der Performance

Wie messen wir aber die Geschwindigkeit eines Programms?

vgl. Projekt im Projektordner unter Nutzung des Pakets

https://www.nuget.org/packages/BenchmarkDotNet

BenchmarkDotNet funktioniert nur, wenn das Konsolenprojekt mit einer Release-Konfiguration erstellt wurde, d. h. mit angewandten Code-Optimierungen. Die Ausführung in Debug führt zu einem Laufzeitfehler.

Implementierung unter C#

using System;
using System.Threading;

class Printer{
  char ch;
  int sleepTime;

  public Printer(char c, int t){
    ch = c;
    sleepTime = t;
  }

  public void Print(){
    for (int i = 0; i<10;  i++){
      Console.Write(ch);
      Thread.Sleep(sleepTime);
    }
  }
}

class Program {
    public static void Main(string[] args){
        Printer a = new Printer ('a', 10);
        Printer b = new Printer ('b', 50);
        Printer c = new Printer ('c', 70);

        var watch = System.Diagnostics.Stopwatch.StartNew();
        a.Print();
        b.Print();
        c.Print();
        watch.Stop();
        Console.WriteLine("\nDuration in ms: {0}", watch.ElapsedMilliseconds);        

        watch.Restart();
        Thread PrinterA = new Thread(new ThreadStart(a.Print));
        Thread PrinterB = new Thread(new ThreadStart(b.Print));
        PrinterA.Start();
        PrinterB.Start();
        c.Print();   // Ausführung im Main-Thread
        watch.Stop();
        Console.WriteLine("\nDuration in ms: {0}", watch.ElapsedMilliseconds);   
    }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

Die Implementierung der Klasse Thread unter C# umfasst dabei folgende Definitionen:

public delegate void ThreadStart();
public delegate void ParameterizedThreadStart(object? obj);
public enum ThreadPriority (Lowest = 0, BelowNormal = 1, Normal = 2, AboveNormal = 3, Highest = 4);
public enum ThreadState (Running = 0, Unstarted = 8, Stopped = 16, Suspended = 64, Stopped = 16, Aborted = 256, ...);

public sealed class Thread{
  public Thread (ThreadStart start);
  public Thread (ParameterizedThreadStart start);
  public Thread (ThreadStart start, int maxStackSize);
  public Thread (ParameterizedThreadStart start, int maxStackSize);
  ...
  public string Name {get; set;};
  public ThreadPriority Priority {get; set;};
  public ThreadState ThreadState {get;};
  public bool IsAlive {get;};
  public bool IsBackground{get;};
  public void Start();
  public void Join();
  public void Interrupt();
  public static void Sleep(int milliseconds);
  public static bool Yield ();
}
using System;
using System.Threading;

class Program
{
    public static void Main(string[] args)
    {
        Console.WriteLine("**********Current Thread Informations***************\n");
        Thread t = Thread.CurrentThread;
        t.Name = "Primary_Thread";
        Console.WriteLine("Thread Name: {0}", t.Name);
        Console.WriteLine("Thread Status: {0}", t.ThreadState);
        Console.WriteLine("Priority: {0}", t.Priority);
        Console.WriteLine("Current application domain: {0}",Thread.GetDomain().FriendlyName);
    }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

Thread-Interaktion

Wie lässt sich eine Serialisierung von Threads realisieren? Im Beispiel soll die Ausführung des "Printers C" erst starten, wenn die beiden anderen Druckaufträge abgearbeitet wurden.

Methode Bedeutung
t.Join() Es wird so lange gewartet, bis der Thread t zum Abschluss gekommen ist.
Thread.Sleep(n) Es wird für n Millisekunden gewartet.
Thread.Yield() Gibt den erteilten Zugriff auf die CPU sofort zurück.
using System;
using System.Threading;

class Printer{
  char ch;
  int sleepTime;
  public Printer(char c, int t){
    ch = c;
    sleepTime = t;
  }
  public void Print(){
    for (int i = 0; i<10;  i++){
      Console.Write(ch);
      //Thread.Sleep(sleepTime);
      Thread.Yield();
    }
  }
}
class Program {
    public static void Main(string[] args){
        Printer a = new Printer ('a', 10);
        Printer b = new Printer ('b', 50);
        Printer c = new Printer ('c', 70);
        Thread PrinterA = new Thread(new ThreadStart(a.Print));
        Thread PrinterB = new Thread(new ThreadStart(b.Print));
        PrinterA.Start();
        PrinterB.Start();
        Thread.Sleep(1000);    // Zeitabhängige Verzögerung des Hauptthreads
        //PrinterA.Join();     // <-
        //PrinterB.Join();
        c.Print();
    }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

Aus dem Gesamtkonzept des Threads ergeben sich mehrere Zustände, in denen sich dieser befinden kann:

@startuml
hide empty description
[*] --> Unstarted
Unstarted --> Running : Start
Running --> WaitSleepJoin  : Thread Blocks
WaitSleepJoin --> Running  : Thread Unblocks
WaitSleepJoin --> StopRequested : Interrupt
Running --> Stopped : Thread Ends
Running --> StopRequested : Interrupt
StopRequested --> Stopped : Thread Ends
@enduml
Zustand Bedeutung
Unstarted Thread ist initialisiert
Running Thread befindet sich gerade in der Ausführung
WaitSleepJoin Thread wird wegen eines Sleep oder eines Join-Befehls nicht ausgeführt. Er nutzt keine Prozessorzeit. Oder der Thread wird blockiert, weil er auf eine Ressource wartet, die von einem anderen Thread gehalten wird.
StopRequested Thread wird zum Stoppen aufgefordert. Dies ist nur für den internen Gebrauch bestimmt.
Stopped Bearbeitung beendet

Jeder Thread umfasst ein Feld vom Typ ThreadState, dass auf verschiedenen Ebenen dessen Parameter abbildet. Das Enum ist dabei als Bitfeld konfiguriert (vgl Doku).

public static ThreadState DetermineThreadState(this ThreadState ts){
  return ts & (ThreadState.Unstarted |
               ThreadState.Running |
               ThreadState.WaitSleepJoin |
               ThreadState.Stopped);

}

Thread-Initialisierung

Wie wird das Thread-Objekt korrekt initialisiert? Viele Tutorials führen Beispiele auf, die wie folgt strukturiert sind, während im obrigen Beispiel der Konstruktoraufruf von Thread einen weiteren Konstruktor ThreadStart adressiert:

Thread threadA = new Thread(ExecuteA);
threadA.Start();
// vs
Thread threadB = new Thread(new ThreadStart(ExecuteB));
using System;
using System.Threading;

class Calc  
{
    int paramA = 0;
    int paramB = 0;

    public Calc(int paramA, int paramB){
      this.paramA = paramA;
      this.paramB = paramB;
    }

    // Static method
    public static void getConst()
    {
        Console.WriteLine("Static funtion const = {0}", 3.14);
    }

    public void process()
    {
        Console.WriteLine("Result = {0}", paramA + paramB);
    }
}

class Program
{
    static void Main()
    {
        // explizite Übergabe des Delegaten auf statische Methode
        ThreadStart threadDelegate = new ThreadStart(Calc.getConst);
        Thread newThread = new Thread(threadDelegate);
        newThread.Start();

        // impliziter Cast zu ThreadStart (gleicher Delegat)
        newThread = new Thread(Calc.getConst);    
        newThread.Start();

        // explizite Übergabe des Delegaten auf Methode
        Calc c = new Calc(5, 6);
        threadDelegate = new ThreadStart(c.process);
        newThread = new Thread(threadDelegate);
        newThread.Start();

        // impliziter Cast zu ThreadStart (gleicher Delegat)
        newThread = new Thread(c.process);
        newThread.Start();
    }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

Der Konstruktor der Klasse Thread hat aber folgende Signatur:

Konstruktor Initialisiert eine neue Thread Klasse ...
Thread(ThreadStart) ... auf der Basis einer Instanz von ThreadStart
Thread(ThreadStart, Int32) ... auf der Basis einer Instanz von ThreadStart unter Angabe der Größe des Stacks in Byte (aufgerundet auf entsprechende Page Size und unter Berücksichtigung der globalen Mindestgröße)
Thread(ParameterizedThreadStart) ... auf der Basis einer Instanz von ParameterizedThreadStart
Thread(ParameterizedThreadStart, Int32) ... auf der Basis einer Instanz von ParameterizedThreadStart unter Angabe der Größe des Stacks
// impliziter Cast zu ParameterizedThreadStart
Thread threadB = new Thread(ExecuteB);   
threadB.Start("abc");

// impliziter Cast und unmittelbarer Start
new Thread(SomeMethod).Start();

Aufgabe: Ergänzen Sie das schon benutzte Beispiel um die Möglichkeit das auszugebene Zeichen als Parameter zu übergeben!

using System;
using System.Threading;

class Printer{
  char ch;
  int sleepTime;

  public Printer(char c, int t){
    ch = c;
    sleepTime = t;
  }

  // Unsere Methode soll nun einen Parameter bekommen
  // public void Print(int count){
  // for (int i = 0; i<count;  i++){
  // public void Print(object? count){
  public void Print(object count){
    for (int i = 0; i<(count as int?);  i++){
      Console.Write(ch);
      Thread.Sleep(sleepTime);
    }
  }
}

class Program {
    public static void Main(string[] args){
        Printer a = new Printer ('a', 10);
        //Thread PrinterA = new Thread(new ThreadStart(a.Print));
        //PrinterA.Start();
        Thread PrinterA = new Thread(new ParameterizedThreadStart(a.Print));
        PrinterA.Start(5);
    }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

Zur Übergabe von mehreren Parametern können Tupel oder Objekte benutzerdefinierter Klassen verwendet werden.

Datenaustausch zwischen Threads

Jeder Thread realisiert dabei seinen eigenen Speicher, so dass die lokalen Variablen separat abgelegt werden. Die Verwendung der lokalen Variablen ist entsprechend geschützt.

using System;
using System.Threading;

class Program
{
    static void Execute(object output){
      int count = 0;
      for (int i = 0; i<10;  i++){
        Console.WriteLine(output + (count++).ToString());
        Thread.Sleep(10);
      }
    }

    public static void Main(string[] args){
        Thread thread_A = new Thread(Execute);
        thread_A.Start("New Thread 1:     ");
        Thread.Sleep(10);
        new Thread(Execute).Start("New Thread 2:     ");
        Execute("MainTread :");
    }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

Auf dem individuellen Stack werden die eigenen Kopien der lokalen Variable count angelegt, so dass die beiden Threads keine Interaktion realisieren.

Was aber, wenn ein Datenaustausch realisiert werden soll? Eine Möglichkeit der Interaktion sind entsprechende Felder innerhalb einer gemeinsamen Objektinstanz.

Welches Problem ergibt sich aber dabei?

using System;
using System.Threading;

class InteractiveThreads
{
  // Gemeinsames Member der Klasse
  //[ThreadStatic] // <- gemeinsames Member innerhalb nur eines Threads, nur auf static anwendbar
  public static int count = 0;

  public void AddOne(){
    count++;
    Console.WriteLine("Nachher {0}", count);
  }
}

class Program
{
    public static void Main(string[] args){
        InteractiveThreads myThreads = new InteractiveThreads();
        for (int i = 0; i<100; i++){
          new Thread(myThreads.AddOne).Start();
        }
        Thread.Sleep(10000);
        Console.WriteLine("\n Fertig {0}", InteractiveThreads.count);
    }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

using System;
using System.Threading;

class Calc  
{
    int paramA = 0;
    public void Inc()
    {
        paramA = paramA + 1;
        Console.WriteLine("Static funtion const = {0}", paramA);
    }
}

class Program
{
    public static void Main(string[] args){
        Calc c = new Calc();
        ThreadStart delThreadA = new ThreadStart(c.Inc);
        Thread newThread_A = new Thread(delThreadA);
        newThread_A.Start();

        ThreadStart delThreadB = new ThreadStart(c.Inc);
        Thread newThread_B = new Thread(delThreadB);
        newThread_B.Start();
    }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

Thread-spezifische Daten in nicht-statischen Kontexten können in ThreadLocal<T> oder AsyncLocal<T> verwaltet werden.

private ThreadLocal<int> threadSpecificData = new ThreadLocal<int>(() => 0);

void ThreadMethod(int initialValue)
{
  threadSpecificData.Value = initialValue;
  //...
  threadSpecificData.Value++;
  //...
}

Locking

Locking und Threadsicherheit sind zentrale Herausforderungen bei der Arbeit mit Multithread-Anwendungen. Wie können wir im vorhergehenden Beispiel sicherstellen, dass zwischen dem Laden von threadcount in ein Register, der Inkrementierung und dem Zurückschreiben nicht ein anderer Thread den Wert zwischenzeitlich manipuliert hat?

Für eine binäre Variable wird dabei von einem Test-And-Set Mechanisms gesprochen der Thread-sicher sein muss. Wie können wir dies erreichen? Die Prüfung und Manipulation muss atomar ausgeführt werden, dass heißt an dieser Stelle darf der ausführende Thread nicht verdrängt werden.

Darauf aufbauend implementiert C# verschiedene Methoden:

Threadsicherheit Bemerkung
"exclusive lock" Alleiniger Zugriff auf einen Codeabschnitt
Monitor Erweiterter lock mit Bedingungsvariablen (Wait, Pulse, PulseAll) zum Warten und Signalisieren von Zustandsänderungen, synchronisierende Zugriffsprozeduren
Mutex (Mutual Exclusion) Prozessübergreifende exklusive (binäre) Sperrung
Semaphor Zugriff auf einen Codeabschnitt durch n Threads oder Prozesse, basierend auf einem Zählermechanismus
static readonly object locker = new object();

lock(locker){
  // kritische Region
}
using System;
using System.Threading;

class InteractiveThreads{
  public int count = 0;
  public void AddOne(){
    lock(this)
    {
       count = count + 1;
       count = count + 1;
       count = count + 1;
       count = count + 1;
    }
    Console.WriteLine("count {0}", count);
  }
}

class Program {
    public static void Main(string[] args){
        InteractiveThreads myThreads = new InteractiveThreads();
        for (int i = 0; i<10; i++){
          new Thread(myThreads.AddOne).Start();
        }
    }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

Hintergrund und Vordergrund-Threads

Threads können als Hintergrund- oder Vordergrundthread definiert sein. Hintergrundthreads unterscheiden sich von Vordergrundthreads durch die Beibehaltung der Ausführungsumgebung nach dem Abschluss. Sobald alle Vordergrundthreads in einem verwalteten Prozess (wobei die EXE-Datei eine verwaltete Assembly ist) beendet sind, beendet das System alle Hintergrundthreads.

using System;
using System.Threading;

class Printer{
  char ch;
  int sleepTime;

  public Printer(char c, int t){
    ch = c;
    sleepTime = t;
  }

  public void Print(){
    for (int i = 0; i<10;  i++){
      Console.Write(ch);
      Thread.Sleep(sleepTime);
    }
  }
}

class Program {
    public static void printThreadProperties(Thread currentThread){
      Console.WriteLine("{0} - {1} - {2}", currentThread.Name,
                                           currentThread.Priority,
                                           currentThread.IsBackground);
    }

    public static void Main(string[] args){
        Thread MainThread = Thread.CurrentThread;
        MainThread.Name = "MainThread";
        printThreadProperties(MainThread);
        Printer a = new Printer ('a', 170);
        Printer b = new Printer ('b', 50);
        Printer c = new Printer ('c', 10);
        Thread PrinterA = new Thread(new ThreadStart(a.Print));
        PrinterA.IsBackground = false;
        Thread PrinterB = new Thread(new ThreadStart(b.Print));
        printThreadProperties(PrinterA);
        printThreadProperties(PrinterB);
        PrinterA.Start();
        PrinterB.Start();
        c.Print();
    }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

Wie verhält sich das Programm, wenn Sie Printer_.IsBackground = true; einfügen?

Threads, die explizit mit der Thread-Klasse erstellt werden, sind standardmäßig Vordergrund-Threads.

Ausnahmebehandlung mit Threads

Ab .NET Framework, Version 2.0, erlaubt die CLR bei den meisten Ausnahmefehlern in Threads deren ordnungsgemäße Fortsetzung. Allerdings ist zu beachten, dass die Fehlerbehandlung innerhalb des Threads zu erfolgen hat. Unbehandelte Ausnahmen auf der Thread-Ebene führen in der Regel zum Abbruch des gesamten Programms.

Verschieben Sie die Fehlerbehandlung in den Thread!

using System;
using System.Threading;

class Program {
  public static void Calculate(object value){ //object? value
    Console.WriteLine(5 / (int)value);        //(int?)value
  }

  public static void Main(string[] args){
    Thread myThread = new Thread (Calculate);
    try{
      myThread.Start(0);
    }
    catch(DivideByZeroException)
    {
      Console.WriteLine("Achtung - Division durch Null");
    }
  }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

Analog kann das Abbrechen eines Threads als Ausnahme erkannt und in einer Behandlungsroutine organsiert werden.

using System;
using System.Threading;

class Program {
  static void Operate(){
        try{
          while (true){
            Thread.Sleep(1000);
            Console.WriteLine("Thread - Ausgabe");
          }
        }
        catch (ThreadInterruptedException){
          Console.WriteLine("Thread interrupted");
        }
  }

  public static void Main(string[] args){
    Thread myThread = new Thread (Operate);
    myThread.Start();
    Thread.Sleep(3000);
    myThread.Interrupt();   // <- Abbruch des Threads   
    Console.WriteLine("fertig");
  }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

Thread-Pool

Wann immer ein neuer Thread gestartet wird, bedarf es einiger 100 Millisekunden, um Speicher anzufordern, ihn zu initialisieren, usw. Diese relativ aufwändige Verfahren wird durch die Nutzung von ThreadPools beschränkt, da diese als wiederverwendbare Threads vorgesehen sind.

Die System.Threading.ThreadPool-Klasse stellt einer Anwendung einen Pool von "Arbeitsthreads" bereit, die vom System verwaltet werden und Ihnen die Möglichkeit bieten, sich mehr auf Anwendungsaufgaben als auf die Threadverwaltung zu konzentrieren.

using System;
using System.Threading;

class Program {
  // This thread procedure performs the task.
  static void Operate(object stateInfo)
  {
      Console.WriteLine("Hello from the thread pool.");
  }

  public static void Main(string[] args){
    ThreadPool.QueueUserWorkItem(Operate);
    //Fügt der Warteschlange eine auszuführende Methode hinzu. 
    //Die Methode wird ausgeführt, wenn ein Thread des Threadpools verfügbar wird
   
    Console.WriteLine("Main thread does some work, then sleeps.");
    Thread.Sleep(1000);
    Console.WriteLine("Main thread exits.");
  }
}

@LIA.eval(["main.cs"], mcs main.cs, mono main.exe)

Das klingt sehr praktisch, was aber sind die Einschränkungen?

  • Für die Threads können keine Namen vergeben werden, damit wird das Debugging ggf. schwieriger.
  • Pooled Threads sind immer Background-Threads
  • Sie können keine individuellen Prioritäten festlegen.
  • Blockierte Threads im Pool senken die entsprechende Performance des Pools