Suche…


Bemerkungen

Ein Thread ist ein Teil eines Programms, das unabhängig von anderen Teilen ausgeführt werden kann. Es kann Aufgaben gleichzeitig mit anderen Threads ausführen. Multithreading ist eine Funktion, mit der Programme eine gleichzeitige Verarbeitung durchführen können, sodass jeweils mehr als ein Vorgang ausgeführt werden kann.

Sie können zum Beispiel Threading verwenden, um einen Timer oder Zähler im Hintergrund zu aktualisieren, während Sie gleichzeitig andere Aufgaben im Vordergrund ausführen.

Multithread-Anwendungen reagieren besser auf Benutzereingaben und sind auch leicht skalierbar, da der Entwickler Threads hinzufügen kann, wenn die Arbeitslast steigt.

Standardmäßig verfügt ein C # -Programm über einen Thread - den Hauptprogramm-Thread. Sekundäre Threads können jedoch erstellt und verwendet werden, um Code parallel zum primären Thread auszuführen. Solche Threads werden Arbeitsthreads genannt.

Um den Betrieb eines Threads zu steuern, delegiert die CLR eine Funktion an das als Thread Scheduler bekannte Betriebssystem. Ein Thread-Scheduler stellt sicher, dass allen Threads die richtige Ausführungszeit zugewiesen wird. Außerdem wird überprüft, ob die blockierten oder gesperrten Threads nicht viel CPU-Zeit beanspruchen.

Der .NET Framework System.Threading Namespace System.Threading die Verwendung von Threads. System.Threading ermöglicht Multithreading durch Bereitstellung einer Reihe von Klassen und Schnittstellen. Neben dem Bereitstellen von Typen und Klassen für einen bestimmten Thread werden auch Typen definiert, in denen eine Sammlung von Threads, eine Timerklasse usw. gespeichert werden. Es bietet auch Unterstützung, indem es den synchronisierten Zugriff auf gemeinsam genutzte Daten ermöglicht.

Thread ist die Hauptklasse im System.Threading Namespace. Andere Klassen umfassen AutoResetEvent , Interlocked , Monitor , Mutex und ThreadPool .

Zu den Delegaten, die im System.Threading Namespace vorhanden sind, gehören ThreadStart , TimerCallback und WaitCallback .

Aufzählungen im System.Threading Namespace umfassen ThreadPriority , ThreadState und EventResetMode .

In .NET Framework 4 und System.Threading.Tasks.Parallel Versionen wird die Multithread-Programmierung durch die Klassen System.Threading.Tasks.Parallel und System.Threading.Tasks.Task , Parallel LINQ (PLINQ), neue parallele Auflistungsklassen in System.Collections.Concurrent einfacher und einfacher System.Collections.Concurrent Namespace und ein neues aufgabenbasiertes Programmiermodell.

Einfache vollständige Threading-Demo

class Program
{
    static void Main(string[] args)
    {
        // Create 2 thread objects.  We're using delegates because we need to pass 
        // parameters to the threads.  
        var thread1 = new Thread(new ThreadStart(() => PerformAction(1)));
        var thread2 = new Thread(new ThreadStart(() => PerformAction(2)));

        // Start the threads running 
        thread1.Start();
        // NB: as soon as the above line kicks off the thread, the next line starts; 
        // even if thread1 is still processing.
        thread2.Start();

        // Wait for thread1 to complete before continuing
        thread1.Join();
        // Wait for thread2 to complete before continuing
        thread2.Join();

        Console.WriteLine("Done");
        Console.ReadKey();
    }

    // Simple method to help demonstrate the threads running in parallel.
    static void PerformAction(int id)
    {
        var rnd = new Random(id);
        for (int i = 0; i < 100; i++)
        {
            Console.WriteLine("Thread: {0}: {1}", id, i);
            Thread.Sleep(rnd.Next(0, 1000));
        }
    }
}

Einfache vollständige Threading-Demo mit Aufgaben

class Program
{
    static void Main(string[] args)
    {
        // Run 2 Tasks.  
        var task1 = Task.Run(() => PerformAction(1)));
        var task2 = Task.Run(() => PerformAction(2)));

        // Wait (i.e. block this thread) until both Tasks are complete.
        Task.WaitAll(new [] { task1, task2 });
        
        Console.WriteLine("Done");
        Console.ReadKey();
    }

    // Simple method to help demonstrate the threads running in parallel.
    static void PerformAction(int id)
    {
        var rnd = new Random(id);
        for (int i = 0; i < 100; i++)
        {
            Console.WriteLine("Task: {0}: {1}", id, i);
            Thread.Sleep(rnd.Next(0, 1000));
        }
    }
}

Expliziter Aufgabenparallismus

    private static void explicitTaskParallism()
    {
        Thread.CurrentThread.Name = "Main";

        // Create a task and supply a user delegate by using a lambda expression. 
        Task taskA = new Task(() => Console.WriteLine($"Hello from task {nameof(taskA)}."));
        Task taskB = new Task(() => Console.WriteLine($"Hello from task {nameof(taskB)}."));

        // Start the task.
        taskA.Start();
        taskB.Start();

        // Output a message from the calling thread.
        Console.WriteLine("Hello from thread '{0}'.",
                          Thread.CurrentThread.Name);
        taskA.Wait();
        taskB.Wait();
        Console.Read();
    }

Implizite Aufgabenparallelität

    private static void Main(string[] args)
    {
        var a = new A();
        var b = new B();
        //implicit task parallelism
        Parallel.Invoke(
            () => a.DoSomeWork(),
            () => b.DoSomeOtherWork()
            );

      }

Einen zweiten Thread erstellen und starten

Wenn Sie mehrere lange Berechnungen durchführen, können Sie sie gleichzeitig in verschiedenen Threads auf Ihrem Computer ausführen. Dazu erstellen wir einen neuen Thread und weisen auf eine andere Methode hin.

using System.Threading;

class MainClass {
    static void Main() {
        var thread = new Thread(Secondary);
        thread.Start();
    }

    static void Secondary() {
        System.Console.WriteLine("Hello World!");
    }
}

Einen Thread mit Parametern starten

using System.Threading;

class MainClass {
    static void Main() {
        var thread = new Thread(Secondary);
        thread.Start("SecondThread");
    }

    static void Secondary(object threadName) {
        System.Console.WriteLine("Hello World from thread: " + threadName);
    }
}

Einen Thread pro Prozessor erstellen

Environment.ProcessorCount Ruft die Anzahl der logischen Prozessoren auf der aktuellen Maschine ab.

Die CLR plant dann jeden Thread zu einem logischen Prozessor. Dies könnte theoretisch jeden Thread auf einem anderen logischen Prozessor, alle Threads auf einem einzelnen logischen Prozessor oder eine andere Kombination bedeuten.

using System;
using System.Threading;

class MainClass {
    static void Main() {
        for (int i = 0; i < Environment.ProcessorCount; i++) {
            var thread = new Thread(Secondary);
            thread.Start(i);
        }
        
    }

    static void Secondary(object threadNumber) {
        System.Console.WriteLine("Hello World from thread: " + threadNumber);
    }
}

Gleichzeitiges Lesen und Schreiben von Daten vermeiden

Manchmal möchten Sie, dass Ihre Threads gleichzeitig Daten freigeben. In diesem Fall ist es wichtig, den Code zu kennen und alle Teile zu sperren, die möglicherweise schief gehen. Ein einfaches Beispiel für die Zählung von zwei Threads ist unten dargestellt.

Hier ist ein gefährlicher (falscher) Code:

using System.Threading;

class MainClass 
{    
    static int count { get; set; }

    static void Main() 
    {
        for (int i = 1; i <= 2; i++)
        {
            var thread = new Thread(ThreadMethod);
            thread.Start(i);
            Thread.Sleep(500);
        }
    }

    static void ThreadMethod(object threadNumber) 
    {
        while (true)
        {
            var temp = count;
            System.Console.WriteLine("Thread " + threadNumber + ": Reading the value of count.");
            Thread.Sleep(1000);
            count = temp + 1;
            System.Console.WriteLine("Thread " + threadNumber + ": Incrementing the value of count to:" + count);
            Thread.Sleep(1000);
        }
    }
}

Sie werden bemerken, anstatt 1,2,3,4,5 zu zählen ... wir zählen 1,1,2,2,3 ...

Um dieses Problem zu beheben, müssen wir den Wert von count sperren , damit mehrere verschiedene Threads nicht gleichzeitig lesen und schreiben können. Durch das Hinzufügen einer Sperre und eines Schlüssels können wir verhindern, dass die Threads gleichzeitig auf die Daten zugreifen.

using System.Threading;

class MainClass
{

    static int count { get; set; } 
    static readonly object key = new object();

    static void Main()
    {
        for (int i = 1; i <= 2; i++)
        {
            var thread = new Thread(ThreadMethod);
            thread.Start(i);
            Thread.Sleep(500);
        }
    }

    static void ThreadMethod(object threadNumber)
    {
        while (true)
        {
            lock (key) 
            {
                var temp = count;
                System.Console.WriteLine("Thread " + threadNumber + ": Reading the value of count.");
                Thread.Sleep(1000);
                count = temp + 1;
                System.Console.WriteLine("Thread " + threadNumber + ": Incrementing the value of count to:" + count);
            }
            Thread.Sleep(1000);
        }
    }
}

Parallel.ForEach-Schleife

Wenn Sie eine foreach-Schleife haben, die Sie beschleunigen möchten, und es Ihnen nichts ausmacht, in welcher Reihenfolge sich die Ausgabe befindet, können Sie sie in eine parallele foreach-Schleife konvertieren.

using System;
using System.Threading;
using System.Threading.Tasks;

public class MainClass {

    public static void Main() {
        int[] Numbers = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
        // Single-threaded
        Console.WriteLine("Normal foreach loop: ");
        foreach (var number in Numbers) {
            Console.WriteLine(longCalculation(number));
        }
        // This is the Parallel (Multi-threaded solution)
        Console.WriteLine("Parallel foreach loop: ");
        Parallel.ForEach(Numbers, number => {
            Console.WriteLine(longCalculation(number));
        });
    }

    private static int longCalculation(int number) {
        Thread.Sleep(1000); // Sleep to simulate a long calculation
        return number * number;
    }
}

Deadlocks (zwei Threads warten aufeinander)

Ein Deadlock tritt auf, wenn zwei oder mehr Threads darauf warten, dass sich eine Ressource fertigstellt oder freigibt, sodass sie für immer warten.

Ein typisches Szenario, in dem zwei Threads auf den Abschluss warten, ist, wenn ein Windows Forms-GUI-Thread auf einen Arbeitsthread wartet und der Arbeitsthread versucht, ein vom GUI-Thread verwaltetes Objekt aufzurufen. Beachten Sie, dass bei diesem Code-Beispiel das Klicken auf button1 dazu führt, dass das Programm hängen bleibt.

private void button1_Click(object sender, EventArgs e)
{
    Thread workerthread= new Thread(dowork);
    workerthread.Start();
    workerthread.Join();
    // Do something after
}

private void dowork()
{
    // Do something before
    textBox1.Invoke(new Action(() => textBox1.Text = "Some Text"));
    // Do something after
}

workerthread.Join() ist ein Aufruf, der den aufrufenden Thread blockiert, bis workerthread abgeschlossen ist. textBox1.Invoke(invoke_delegate) ist ein Aufruf, der den aufrufenden Thread blockiert, bis der GUI-Thread invoke_delegate verarbeitet hat. Dieser Aufruf verursacht jedoch Deadlocks, wenn der GUI-Thread bereits auf den Abschluss des aufrufenden Threads wartet.

Um dies zu umgehen, können Sie stattdessen eine nicht blockierende Methode zum Aufrufen des Textfelds verwenden:

private void dowork()
{
    // Do work
    textBox1.BeginInvoke(new Action(() => textBox1.Text = "Some Text"));
    // Do work that is not dependent on textBox1 being updated first
}

Dies führt jedoch zu Problemen, wenn Sie Code ausführen müssen, der vom Textfeld abhängt, das zuerst aktualisiert wird. In diesem Fall führen Sie das als Teil des Aufrufs aus. Beachten Sie jedoch, dass dies auf dem GUI-Thread ausgeführt wird.

private void dowork()
{
    // Do work
    textBox1.BeginInvoke(new Action(() => {
        textBox1.Text = "Some Text";
        // Do work dependent on textBox1 being updated first, 
        // start another worker thread or raise an event
    }));
    // Do work that is not dependent on textBox1 being updated first
}

Starten Sie alternativ einen ganzen neuen Thread, und warten Sie auf den GUI-Thread, damit der Arbeitsthread möglicherweise abgeschlossen wird.

private void dowork()
{
    // Do work
    Thread workerthread2 = new Thread(() =>
    {
        textBox1.Invoke(new Action(() => textBox1.Text = "Some Text"));
        // Do work dependent on textBox1 being updated first, 
        // start another worker thread or raise an event
    });
    workerthread2.Start();
    // Do work that is not dependent on textBox1 being updated first
}

Vermeiden Sie, wenn möglich, Zirkelverweise zwischen Threads, um das Risiko eines gegenseitigen Wartens zu minimieren. Eine Hierarchie von Threads, in denen untergeordnete Threads nur Nachrichten für übergeordnete Threads hinterlassen und nicht auf sie warten, wird nicht zu diesem Problem führen. Es ist jedoch immer noch anfällig für Deadlocks, die auf dem Sperren von Ressourcen basieren.

Deadlocks (Ressource halten und warten)

Ein Deadlock tritt auf, wenn zwei oder mehr Threads darauf warten, dass sich eine Ressource fertigstellt oder freigibt, sodass sie für immer warten.

Wenn Thread1 eine Sperre für Ressource A hält und auf die Freigabe der Ressource B wartet, während Thread2 die Ressource B enthält und auf die Freigabe der Ressource A wartet, sind sie blockiert.

Durch Klicken auf button1 für den folgenden Beispielcode wird Ihre Anwendung in den oben genannten Deadlock-Zustand versetzt und hängt

private void button_Click(object sender, EventArgs e)
{
    DeadlockWorkers workers = new DeadlockWorkers();
    workers.StartThreads();
    textBox.Text = workers.GetResult();
}

private class DeadlockWorkers
{
    Thread thread1, thread2;

    object resourceA = new object();
    object resourceB = new object();

    string output;

    public void StartThreads()
    {
        thread1 = new Thread(Thread1DoWork);
        thread2 = new Thread(Thread2DoWork);
        thread1.Start();
        thread2.Start();
    }

    public string GetResult()
    {
        thread1.Join();
        thread2.Join();
        return output;
    }

    public void Thread1DoWork()
    {
        Thread.Sleep(100);
        lock (resourceA)
        {
            Thread.Sleep(100);
            lock (resourceB)
            {
                output += "T1#";
            }
        }
    }

    public void Thread2DoWork()
    {
        Thread.Sleep(100);
        lock (resourceB)
        {
            Thread.Sleep(100);
            lock (resourceA)
            {
                output += "T2#";
            }
        }
    }
}

Um auf diese Weise nicht blockiert zu werden, können Sie Monitor.TryEnter (lock_object, timeout_in_milliseconds) verwenden, um zu prüfen, ob bereits ein Objekt gesperrt ist. Wenn es Monitor.TryEnter nicht gelingt, vor timeout_in_milliseconds eine Sperre für lock_object zu erlangen, gibt es false zurück, wodurch der Thread die Möglichkeit erhält, andere angehaltene Ressourcen freizugeben und nachgibt, wodurch andere Threads die Möglichkeit erhalten, wie in dieser leicht modifizierten Version des obigen Beispiels abzuschließen :

private void button_Click(object sender, EventArgs e)
{
    MonitorWorkers workers = new MonitorWorkers();
    workers.StartThreads();
    textBox.Text = workers.GetResult();
}

private class MonitorWorkers
{
    Thread thread1, thread2;

    object resourceA = new object();
    object resourceB = new object();

    string output;

    public void StartThreads()
    {
        thread1 = new Thread(Thread1DoWork);
        thread2 = new Thread(Thread2DoWork);
        thread1.Start();
        thread2.Start();
    }

    public string GetResult()
    {
        thread1.Join();
        thread2.Join();
        return output;
    }

    public void Thread1DoWork()
    {
        bool mustDoWork = true;
        Thread.Sleep(100);
        while (mustDoWork)
        {
            lock (resourceA)
            {
                Thread.Sleep(100);
                if (Monitor.TryEnter(resourceB, 0))
                {
                    output += "T1#";
                    mustDoWork = false;
                    Monitor.Exit(resourceB);
                }
            }
            if (mustDoWork) Thread.Yield();
        }
    }

    public void Thread2DoWork()
    {
        Thread.Sleep(100);
        lock (resourceB)
        {
            Thread.Sleep(100);
            lock (resourceA)
            {
                output += "T2#";
            }
        }
    }
}

Beachten Sie, dass diese Problemumgehung darauf beruht, dass Thread2 hartnäckig ist, weil seine Sperren und Thread1 nachgeben wollen, so dass Thread2 immer Vorrang hat. Beachten Sie auch, dass Thread1 die Arbeit, die er nach dem Sperren der Ressource A ausgeführt hat, wiederholen muss, wenn er nachgibt. Seien Sie daher vorsichtig, wenn Sie diesen Ansatz mit mehr als einem nachgiebigen Thread implementieren, da Sie Gefahr laufen, in einen sogenannten Livelock einzutreten - ein Zustand, der eintreten würde, wenn zwei Threads den ersten Teil ihrer Arbeit erledigen und sich dann gegenseitig nachgeben , wiederholt wiederholt.



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow