Поиск…


замечания

Поток является частью программы, которая может выполняться независимо от других частей. Он может выполнять задачи одновременно с другими потоками. Многопоточность - это функция, которая позволяет программам выполнять параллельную обработку, чтобы одновременно выполнять несколько операций.

Например, вы можете использовать потоки для обновления таймера или счетчика в фоновом режиме, одновременно выполняя другие задачи на переднем плане.

Многопоточные приложения более восприимчивы к пользовательскому вводу, а также легко масштабируются, поскольку разработчик может добавлять потоки по мере увеличения рабочей нагрузки.

По умолчанию программа C # имеет один поток - основной поток программы. Тем не менее, вторичные потоки могут быть созданы и использованы для выполнения кода параллельно с основным потоком. Такие потоки называются рабочими потоками.

Для управления работой потока CLR делегирует функцию в операционную систему, известную как Thread Scheduler. Планировщик потоков гарантирует, что всем потокам будет назначено надлежащее время выполнения. Он также проверяет, что потоки, которые заблокированы или заблокированы, не потребляют большую часть времени процессора.

Пространство имен .NET Framework System.Threading упрощает использование потоков. System.Threading позволяет многопоточность, предоставляя несколько классов и интерфейсов. Помимо предоставления типов и классов для определенного потока, он также определяет типы для хранения коллекции потоков, класса таймера и т. Д. Он также обеспечивает поддержку, позволяя синхронизировать доступ к общим данным.

Thread является основным классом в пространстве имен System.Threading . Другие классы включают AutoResetEvent , Interlocked , Monitor , Mutex и ThreadPool .

Некоторые из делегатов, присутствующих в пространстве имен System.Threading включают ThreadStart , TimerCallback и WaitCallback .

Перечисления в пространстве имен System.Threading включают ThreadPriority , ThreadState и EventResetMode .

В .NET Framework 4 и более поздних версиях многопоточное программирование упрощается и упрощается с помощью классов System.Threading.Tasks.Parallel и System.Threading.Tasks.Task , Parallel LINQ (PLINQ), новых классов параллельной коллекции в System.Collections.Concurrent имен System.Collections.Concurrent и новая модель программирования на основе задач.

Простая полная версия 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));
        }
    }
}

Простая полная демонстрация потоков с использованием задач

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

Явный целевой паралич

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

Неявный параллелизм задач

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

      }

Создание и запуск второй темы

Если вы выполняете несколько длинных вычислений, вы можете запускать их одновременно в разных потоках на вашем компьютере. Для этого мы создаем новый поток и указываем на другой метод.

using System.Threading;

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

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

Запуск потока с параметрами

используя 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);
    }
}

Создание одной нити на процессор

Environment.ProcessorCount Возвращает количество логических процессоров на текущем компьютере.

Затем CLR будет планировать каждый поток на логическом процессоре, это теоретически может означать каждый поток на другом логическом процессоре, все потоки на одном логическом процессоре или какую-то другую комбинацию.

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

Избегание одновременного чтения и записи данных

Иногда вы хотите, чтобы ваши потоки одновременно обменивались данными. Когда это происходит, важно знать код и блокировать любые части, которые могут пойти не так. Ниже приведен простой пример подсчета двух потоков.

Вот какой-то опасный (неправильный) код:

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

Вы заметите, вместо подсчета 1,2,3,4,5 ... мы считаем 1,1,2,2,3 ...

Чтобы устранить эту проблему, нам нужно заблокировать значение count, чтобы несколько разных потоков не могли читать и писать на него одновременно. С добавлением блокировки и ключа мы можем предотвратить потоки доступа к данным одновременно.

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

Параллельный цикл forEach

Если у вас есть цикл foreach, который вы хотите ускорить, и вы не возражаете против того, в каком порядке находится выход, вы можете преобразовать его в параллельный цикл foreach, выполнив следующие действия:

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

Тупики (два потока, ожидающие друг друга)

Тупик - это то, что происходит, когда два или более потока ожидают, что каждый может завершить или выпустить ресурс таким образом, что они будут ждать навсегда.

Типичный сценарий двух потоков, ожидающих завершения каждого из них, - это когда поток графического интерфейса Windows Forms ожидает рабочий поток, а рабочий поток пытается вызвать объект, управляемый потоком графического интерфейса пользователя. Обратите внимание, что с помощью этого кода, нажатие кнопки1 приведет к зависанию программы.

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() - это вызов, который блокирует вызывающий поток до тех пор, пока workthread не завершится. textBox1.Invoke(invoke_delegate) - это вызов, который блокирует вызывающий поток до тех пор, пока поток GUI не обработает invoke_delegate, но этот вызов вызывает взаимоблокировки, если поток GUI уже ожидает завершения вызова.

Чтобы обойти это, можно использовать неблокирующий способ вызова текстового поля:

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

Однако это вызовет проблемы, если вам нужно запустить код, зависящий от первого обновляемого текстового поля. В этом случае запустите это как часть вызова, но имейте в виду, что это запустит его в потоке графического интерфейса.

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
}

В качестве альтернативы, запустите весь новый поток, и пусть это сделает ожидание в потоке графического интерфейса, так что workthread может завершиться.

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
}

Чтобы свести к минимуму риск захода в тупик взаимного ожидания, всегда избегайте циркулярных ссылок между нитями, когда это возможно. Иерархия потоков, в которых потоки нижнего уровня оставляют сообщения только для высокопоставленных потоков и никогда не ждут от них, не будут сталкиваться с такой проблемой. Однако он все равно будет уязвим для блокировок, основанных на блокировке ресурсов.

Тупики (удерживайте ресурс и подождите)

Тупик - это то, что происходит, когда два или более потока ожидают, что каждый может завершить или выпустить ресурс таким образом, что они будут ждать навсегда.

Если thread1 удерживает блокировку на ресурсе A и ожидает, пока ресурс B будет выпущен, а thread2 будет содержать ресурс B и ожидает освобождения ресурса A, они блокируются.

Нажатие кнопки1 для следующего примера кода приведет к тому, что ваше приложение войдет в вышеупомянутое тупиковое состояние и повесит

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

Чтобы избежать блокировки таким образом, можно использовать Monitor.TryEnter (lock_object, timeout_in_milliseconds), чтобы проверить, не заблокирован ли замок на объекте. Если Monitor.TryEnter не удалось получить блокировку lock_object до timeout_in_milliseconds, он возвращает false, давая потоку возможность освободить другие удерживаемые ресурсы и уступить, тем самым предоставив другим потокам возможность завершить, как в этой слегка модифицированной версии выше :

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

Обратите внимание, что это обходное решение полагается на то, что thread2 упрям ​​в отношении его блокировок и thread1, которые готовы дать, так что thread2 всегда имеет приоритет. Также обратите внимание, что thread1 должен повторить работу, которую он выполнил после блокировки ресурса A, когда он дает. Поэтому будьте осторожны при реализации этого подхода с более чем одним выходным потоком, так как тогда вы рискуете попасть в так называемый livelock - состояние, которое произойдет, если два потока продолжат делать первый бит их работы, а затем взаимно , начиная многократно.



Modified text is an extract of the original Stack Overflow Documentation
Лицензировано согласно CC BY-SA 3.0
Не связан с Stack Overflow