Szukaj…


Uwagi

Wątek jest częścią programu, który może być uruchamiany niezależnie od innych części. Może wykonywać zadania jednocześnie z innymi wątkami. Wielowątkowość to funkcja, która umożliwia programom wykonywanie jednoczesnego przetwarzania, dzięki czemu można wykonać więcej niż jedną operację na raz.

Na przykład możesz użyć wątków, aby zaktualizować licznik czasu lub licznik w tle, jednocześnie wykonując inne zadania na pierwszym planie.

Aplikacje wielowątkowe reagują w większym stopniu na dane wprowadzane przez użytkowników, a także są łatwo skalowalne, ponieważ programista może dodawać wątki w miarę zwiększania obciążenia.

Domyślnie program C # ma jeden wątek - główny wątek programu. Można jednak utworzyć wątki wtórne i wykorzystać je do wykonania kodu równolegle z wątkiem podstawowym. Takie wątki nazywane są wątkami roboczymi.

Aby kontrolować działanie wątku, CLR deleguje funkcję do systemu operacyjnego znanego jako Thread Scheduler. Program do planowania wątków zapewnia, że wszystkie wątki mają przydzielony właściwy czas wykonania. Sprawdza również, czy wątki, które są zablokowane lub zablokowane, nie zajmują dużo czasu procesora.

Przestrzeń nazw .NET Framework System.Threading ułatwia korzystanie z wątków. System.Threading umożliwia wielowątkowość, zapewniając szereg klas i interfejsów. Oprócz zapewniania typów i klas dla określonego wątku, definiuje również typy do przechowywania kolekcji wątków, klasy timera i tak dalej. Zapewnia również wsparcie, umożliwiając zsynchronizowany dostęp do udostępnionych danych.

Thread jest główną klasą w przestrzeni nazw System.Threading . Inne klasy obejmują AutoResetEvent , Interlocked , Monitor , Mutex i ThreadPool .

Niektóre delegaty, które są obecne w przestrzeni nazw System.Threading obejmują ThreadStart , TimerCallback i WaitCallback .

Wyliczenia w przestrzeni nazw System.Threading obejmują ThreadPriority , ThreadState i EventResetMode .

W .NET Framework 4 i nowszych wersjach programowanie wielowątkowe jest łatwiejsze i prostsze dzięki System.Threading.Tasks.Parallel i System.Threading.Tasks.Task , Parallel LINQ (PLINQ), nowe współbieżne klasy kolekcji w System.Collections.Concurrent przestrzeń nazw i nowy model programowania zadaniowego.

Prosta pełna prezentacja wątków

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

Prosta pełna prezentacja wątków za pomocą zadań

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

Jawny paralizm zadań

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

Niejawna równoległość zadań

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

      }

Tworzenie i rozpoczynanie drugiego wątku

Jeśli wykonujesz wiele długich obliczeń, możesz uruchomić je jednocześnie w różnych wątkach na komputerze. Aby to zrobić, tworzymy nowy wątek i wskazujemy inną metodę.

using System.Threading;

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

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

Rozpoczęcie wątku z parametrami

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

Tworzenie jednego wątku na procesor

Environment.ProcessorCount Pobiera liczbę procesorów logicznych na bieżącym komputerze.

CLR zaplanuje następnie każdy wątek do procesora logicznego, teoretycznie może to oznaczać każdy wątek na innym procesorze logicznym, wszystkie wątki na jednym procesorze logicznym lub inną kombinację.

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

Unikanie jednoczesnego odczytu i zapisu danych

Czasami chcesz, aby Twoje wątki jednocześnie udostępniały dane. Kiedy tak się dzieje, ważne jest, aby znać kod i blokować wszystkie części, które mogą się nie udać. Prosty przykład liczenia dwóch wątków pokazano poniżej.

Oto niebezpieczny (niepoprawny) kod:

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

Zauważysz, zamiast liczyć 1,2,3,4,5 ... liczymy 1,1,2,2,3 ...

Aby rozwiązać ten problem, musimy zablokować wartość count, aby wiele różnych wątków nie mogło jednocześnie czytać i zapisywać. Dodając blokadę i klucz, możemy uniemożliwić wątkom jednoczesny dostęp do danych.

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 Loop

Jeśli masz pętlę foreach, którą chcesz przyspieszyć, i nie masz nic przeciwko, w jakiej kolejności jest wyjście, możesz przekonwertować ją na równoległą pętlę foreach, wykonując następujące czynności:

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

Zakleszczenia (dwa wątki oczekujące na siebie)

Zakleszczenie występuje, gdy dwa lub więcej wątków czeka na siebie nawzajem, aby zakończyć lub zwolnić zasób w taki sposób, że będą czekać wiecznie.

Typowy scenariusz, w którym dwa wątki oczekują na zakończenie, gdy wątek GUI formularzy Windows Forms czeka na wątek roboczy, a wątek roboczy próbuje wywołać obiekt zarządzany przez wątek GUI. Zauważ, że w tym przykładzie kodu kliknięcie przycisku 1 spowoduje zawieszenie się programu.

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() to wywołanie, które blokuje wątek wywołujący, aż do momentu ukończenia workthread. textBox1.Invoke(invoke_delegate) to wywołanie, które blokuje wątek wywołujący, dopóki wątek GUI nie przetworzy invoke_delegate, ale to wywołanie powoduje zakleszczenia, jeśli wątek GUI już czeka na zakończenie wątku wywołującego.

Aby obejść ten problem, można zamiast tego użyć nieblokującego sposobu wywoływania pola tekstowego:

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

Spowoduje to jednak problemy, jeśli konieczne będzie uruchomienie kodu, który zależy od pierwszej aktualizacji pola tekstowego. W takim przypadku uruchom go jako część invoke, ale pamiętaj, że spowoduje to uruchomienie go w wątku GUI.

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
}

Ewentualnie rozpocznij od zupełnie nowego wątku i pozwól temu wykonać oczekiwanie w wątku GUI, aby wątek roboczy mógł się zakończyć.

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
}

Aby zminimalizować ryzyko wpadnięcia w impas wzajemnego oczekiwania, zawsze unikaj okrągłych odniesień między wątkami, jeśli to możliwe. Hierarchia wątków, w której wątki niższej rangi pozostawiają wiadomości tylko dla wątków wyższego rangi i nigdy na nich nie czekają, nie napotka takiego problemu. Jednak nadal byłby podatny na zakleszczenia wynikające z blokowania zasobów.

Zakleszczenia (wstrzymaj zasób i poczekaj)

Zakleszczenie występuje, gdy dwa lub więcej wątków czeka na siebie nawzajem, aby zakończyć lub zwolnić zasób w taki sposób, że będą czekać wiecznie.

Jeśli wątek 1 blokuje zasób A i czeka na uwolnienie zasobu B, podczas gdy wątek 2 utrzymuje zasób B i czeka na uwolnienie zasobu A, są one zakleszczone.

Kliknięcie przycisku 1 w celu wyświetlenia poniższego przykładowego kodu spowoduje przejście aplikacji do wspomnianego stanu zakleszczenia i zawieszenie się

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

Aby uniknąć zakleszczenia w ten sposób, można użyć Monitor.TryEnter (lock_object, timeout_in_milliseconds), aby sprawdzić, czy blokada jest już utrzymywana na obiekcie. Jeśli Monitor.TryEnter nie uda się uzyskać blokady na obiekcie lock_object przed upływem limitu czasu w milisekundach, zwraca wartość false, dając wątkowi szansę na zwolnienie innych przechowywanych zasobów i poddanie się, dając tym samym szansę innym wątkom na ukończenie, jak w tej nieco zmodyfikowanej wersji powyższej :

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

Zauważ, że to obejście polega na tym, że wątek 2 jest uparty przy swoich blokadach i wątek 1 jest skłonny ustąpić, tak że wątek 2 zawsze ma pierwszeństwo. Zauważ też, że wątek 1 musi przerobić pracę wykonaną po zablokowaniu zasobu A, gdy się poddaje. Dlatego należy zachować ostrożność przy wdrażaniu tego podejścia z więcej niż jednym podatnym wątkiem, ponieważ wówczas ryzykujesz wejście do tak zwanego blokady livelock - stanu, który miałby miejsce, gdyby dwa wątki wykonywały pierwszą część swojej pracy, a następnie ustępowały wzajemnie , zaczynając wielokrotnie.



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow