Zoeken…


Opmerkingen

Een thread is een onderdeel van een programma dat onafhankelijk van andere onderdelen kan worden uitgevoerd. Het kan taken tegelijkertijd uitvoeren met andere threads. Multithreading is een functie waarmee programma's gelijktijdige verwerking kunnen uitvoeren, zodat er meerdere bewerkingen tegelijk kunnen worden uitgevoerd.

U kunt bijvoorbeeld threading gebruiken om een timer of teller op de achtergrond bij te werken terwijl u tegelijkertijd andere taken op de voorgrond uitvoert.

Multithreaded applicaties reageren sneller op gebruikersinvoer en zijn ook gemakkelijk schaalbaar, omdat de ontwikkelaar threads kan toevoegen naarmate de werklast toeneemt.

Standaard heeft een C # -programma één thread - de hoofdprogrammathread. Er kunnen echter secundaire threads worden gemaakt en gebruikt om code parallel met de primaire thread uit te voeren. Dergelijke threads worden werkerdraden genoemd.

Om de werking van een thread te besturen, delegeert de CLR een functie aan het besturingssysteem dat Thread Scheduler wordt genoemd. Een thread-planner zorgt ervoor dat aan alle threads de juiste uitvoeringstijd wordt toegewezen. Het controleert ook of de geblokkeerde of vergrendelde threads niet veel CPU-tijd verbruiken.

De .NET Framework System.Threading naamruimte maakt het gebruik van threads eenvoudiger. System.Threading maakt multithreading mogelijk door een aantal klassen en interfaces te bieden. Naast het bieden van typen en klassen voor een bepaalde thread, definieert het ook typen voor een verzameling threads, timerklasse enzovoort. Het biedt ook ondersteuning door gesynchroniseerde toegang tot gedeelde gegevens toe te staan.

Thread is de hoofdklasse in de naamruimte System.Threading . Andere klassen zijn AutoResetEvent , Interlocked , Monitor , Mutex en ThreadPool .

Sommige van de afgevaardigden die aanwezig zijn in de System.Threading naamruimte zijn ThreadStart , TimerCallback en WaitCallback .

Tellingen in System.Threading naamruimte omvatten ThreadPriority , ThreadState en EventResetMode .

In .NET Framework 4 en latere versies wordt multithreaded programmeren eenvoudiger en eenvoudiger gemaakt via de System.Threading.Tasks.Parallel en System.Threading.Tasks.Task klassen, Parallel LINQ (PLINQ), nieuwe gelijktijdige verzamelklassen in de System.Collections.Concurrent naamruimte en een nieuw taakgebaseerd programmeermodel.

Eenvoudige complete 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));
        }
    }
}

Eenvoudige complete threading-demo met behulp van taken

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));
        }
    }
}

Expliciet taakparallisme

    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();
    }

Impliciete taakparallellisme

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

      }

Een tweede thread maken en starten

Als u meerdere lange berekeningen uitvoert, kunt u deze tegelijkertijd op verschillende threads op uw computer uitvoeren. Om dit te doen, maken we een nieuwe draad en laten we deze naar een andere methode verwijzen.

using System.Threading;

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

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

Een thread starten met parameters

met behulp van 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);
    }
}

Eén thread per processor maken

Environment.ProcessorCount Hiermee wordt het aantal logische processors op de huidige machine opgehaald.

De CLR plant dan elke thread naar een logische processor, dit zou theoretisch kunnen betekenen dat elke thread op een andere logische processor, alle threads op een enkele logische processor of een andere combinatie.

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);
    }
}

Vermijden lezen en schrijven van gegevens tegelijkertijd

Soms wilt u dat uw threads tegelijkertijd gegevens delen. Wanneer dit gebeurt, is het belangrijk om op de hoogte te zijn van de code en alle onderdelen te vergrendelen die fout kunnen gaan. Een eenvoudig voorbeeld van het tellen van twee threads wordt hieronder getoond.

Hier is een gevaarlijke (onjuiste) 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);
        }
    }
}

U zult merken dat in plaats van 1,2,3,4,5 ... we 1,1,2,2,3 tellen ...

Om dit probleem op te lossen, moeten we de waarde van count vergrendelen , zodat meerdere verschillende threads er niet tegelijkertijd naar kunnen lezen en naar kunnen schrijven. Met de toevoeging van een slot en een sleutel kunnen we voorkomen dat de threads tegelijkertijd toegang hebben tot de gegevens.

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. Voor elke lus

Als je een foreach-lus hebt die je wilt versnellen en het niet erg vindt in welke volgorde de uitvoer zich bevindt, kun je deze als volgt omzetten in een parallelle foreach-lus:

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 (twee threads wachten op elkaar)

Een impasse is wat er gebeurt wanneer twee of meer threads op elkaar wachten om een resource te voltooien of zodanig vrij te geven dat ze voor altijd wachten.

Een typisch scenario waarbij twee threads op elkaar wachten om te voltooien, is wanneer een Windows Forms GUI-thread wacht op een werkthread en de werkthread probeert een object op te roepen dat wordt beheerd door de GUI-thread. Merk op dat met deze code-exmaple, het klikken op knop1 ervoor zorgt dat het programma vastloopt.

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() is een aanroep die de aanroepende thread blokkeert totdat workerthread is voltooid. textBox1.Invoke(invoke_delegate) is een aanroep die de aanroepende thread blokkeert totdat de GUI-thread invoke_delegate heeft verwerkt, maar deze aanroep veroorzaakt deadlocks als de GUI-thread al wacht op de voltooiing van de aanroepende thread.

Om dit te omzeilen, kan men in plaats daarvan een niet-blokkerende manier gebruiken om het tekstvak aan te roepen:

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

Dit veroorzaakt echter problemen als u code moet uitvoeren die afhankelijk is van het tekstvak dat eerst wordt bijgewerkt. Voer dat in dat geval uit als onderdeel van de aanroep, maar houd er rekening mee dat het hierdoor op de GUI-thread wordt uitgevoerd.

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
}

U kunt ook een hele nieuwe thread starten en die laten wachten op de GUI-thread, zodat de werkdraad kan worden voltooid.

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
}

Vermijd cirkelvormige verwijzingen tussen threads indien mogelijk om het risico op een impasse van wederzijds wachten te minimaliseren. Een hiërarchie van threads waarbij threads van een lagere rang alleen berichten achterlaten voor threads van een hogere rang en er nooit op wachten, zal dit soort problemen niet tegenkomen. Het zou echter nog steeds kwetsbaar zijn voor deadlocks op basis van bronvergrendeling.

Deadlocks (bron vasthouden en wachten)

Een impasse is wat er gebeurt wanneer twee of meer threads op elkaar wachten om een resource te voltooien of zodanig vrij te geven dat ze voor altijd wachten.

Als thread1 een lock op resource A vasthoudt en wacht tot resource B wordt vrijgegeven, terwijl thread2 resource B vasthoudt en wacht op resource A wordt vrijgegeven, zijn ze vastgelopen.

Als u op knop1 voor de volgende voorbeeldcode klikt, wordt uw toepassing in de bovengenoemde impasse-toestand gebracht en loopt deze vast

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#";
            }
        }
    }
}

Om te voorkomen dat u op deze manier vastloopt, kunt u Monitor.DoEnter (lock_object, timeout_in_milliseconds) gebruiken om te controleren of er al een vergrendeling op een object aanwezig is. Als Monitor.TryEnter er niet in slaagt een lock op lock_object te verkrijgen vóór timeout_in_milliseconds, geeft het false terug, waardoor de thread een kans krijgt om andere vastgehouden bronnen vrij te geven en te geven, waardoor andere threads een kans krijgen om te voltooien zoals in deze licht gewijzigde versie van de bovenstaande :

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#";
            }
        }
    }
}

Merk op dat deze oplossing afhankelijk is van thread2 die koppig is om zijn sloten en thread1 bereid is te geven, zodat thread2 altijd voorrang heeft. Merk ook op dat thread1 het werk dat het deed na het vergrendelen van resource A opnieuw moet doen wanneer het oplevert. Wees daarom voorzichtig bij het implementeren van deze aanpak met meer dan één meegevende thread, omdat je dan het risico loopt om een zogenaamde livelock binnen te gaan - een toestand die zou optreden als twee threads het eerste deel van hun werk blijven doen en dan wederzijds meegeven , herhaaldelijk opnieuw beginnen.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow