Руководство по программированию многопоточности C#

1- Понятие о многопоточности (Multithreading)

Многопоточность это важное понятия в языке программирования, так же как и C#, это способ создать параллельную обработку. Для ясности взгляните на следующий пример:
HelloThread.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class HelloThread
    {
        public static void Main(string[] args)
        {
             
            Console.WriteLine("Create new Thread...\n");          

            // Создать дочерний Thread, для одновременного запуска с главным Thread (main thread).
            Thread newThread = new Thread(WriteB);

            Console.WriteLine("Start newThread...\n");

            // Запустить newThread.
            newThread.Start();

            Console.WriteLine("Call Write('-') in main Thread...\n");

            // В главном Thread записать символы '-'
            for (int i = 0; i < 50; i++)
            {
                Console.Write('-');

                // Спать (sleep) 70 милисекунд.
                Thread.Sleep(70);
            }


            Console.WriteLine("Main Thread finished!\n");
            Console.Read();
        }



        public static void WriteB()
        {
            // Цикл записывает символ  'B' 100 раз
            for (int i = 0; i < 100; i++)
            {
                Console.Write('B');

                // Спать 100 милисекунд
                Thread.Sleep(100);
            }
                
        }
    }

   
}
Запускаете следующий класс:
Правило работы потока (Thread) объясняется в следующей иллюстрации:

2- Передать параметры в Thread

Перед этим вы ознакомились с примером  HelloThread, вы создали упакованный объект (wrap) статический метод выполнения данного метода параллельно с главным потоком (thread).
Статический метод может стать параметром переданным в Constructor класса  Thread если метод не имеет параметров, или единственный параметр вида объекта  object.
// Статический метод, не имеет параметра.
public static void LetGo()
{
      // ...
}

// Статический метод имеет 1 единственный параметр вида object.
public static void GetGo(object value)
{
      // ...
}
В следующем примере, я создам один  Thread чтобы упаковать статичский метод с одним параметром (object type). Запускаю thread и передаю значение параметру.
MyWork.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class MyWork
    {

        public static void DoWork(object ch)
        {
            for (int i = 0; i < 100; i++)
            {
            	 
                Console.Write(ch);

                // Cпать (sleep) 50 милисекунд.
                Thread.Sleep(50);
            }
        }



    }
}
ThreadParamDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class ThreadParamDemo
    {

        public static void Main(string[] args)
        { 

            Console.WriteLine("Create new thread.. \n");

            // Создать объект Thread обертывающий (wrap) статический метод MyWork.DoWork
            Thread workThread = new Thread(MyWork.DoWork);

            Console.WriteLine("Start workThread...\n");

            // Запустить workThread,
            // и передать в параметр для метода MyWork.DoWork.
            workThread.Start("*");


            for (int i = 0; i < 20; i++)
            {
                Console.Write(".");

                // Спать 30 секунды.
                Thread.Sleep(30);
            }

            Console.WriteLine("MainThread ends");
            Console.Read();
        }
    }

}
Запуск класса  ThreadParamDemo:

3- Thread использует нестатический метод

Вы так же можете создать поток (thread) используя стандартные методы. Например:
Worker.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class Worker
    {
        private string name;
        private int loop;

        public Worker(string name, int loop)
        {
            this.name = name;
            this.loop = loop;
        }

        public void DoWork(object value)
        {
            for (int i = 0; i < loop; i++)
            {
                Console.WriteLine(name + " working " + value);
                Thread.Sleep(50);
            }
             
        }

    }

}
WorkerTest.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class WorkerTest
    {

        public static void Main(string[] args)
        {
            Worker w1 = new Worker("Tran",10);

            // Создать объект Thread.
            Thread workerThread1 = new Thread(w1.DoWork);

            // Передать параметр для метода DoWork.
            workerThread1.Start("A");


            Worker w2 = new Worker("Marry",15);

            // Создать объект Thread.
            Thread workerThread2 = new Thread(w2.DoWork);

            // Передать параметр для метода DoWork.
            workerThread2.Start("B"); 
          
            Console.Read();
        }
    }

}
Запуск примера:

4- ThreadStart Delegate

ThreadStart это класс (Delegate), инциируется методом упаковки. Передан как параметро для инициализации объетка Thread.
 С  .Net < 2.0, чтобы стартовать (start) один поток (thread), вам нужно создать  ThreadStart, он является delegate.

Начиная с версии 2.0  .NET Framework, не нужно создавать отдельный делегат ( ThreadStart). Вам нужно только указать имя метода в constructor у  Thread.
 
Programmer.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class Programmer
    {
        private string name;
        public Programmer(string name)  
        {
            this.name= name;
        }

        // Это нестатический метод, не имеет параметра.
        public void DoCode()
        {
            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine(name + " codding ... ");
                Thread.Sleep(50);
            }
                
        }
    }

}
ThreadStartDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;


namespace MultithreadingTutorial
{
    class ThreadStartDemo
    {


        public static void Main(string[] args)
        {
            // Создать объект ThreadStart обертывает 1 статический метод.
            // (Он только обертывает методы без параметра)
            // (Он является объектом авторизированным для выполнения метода).
            ThreadStart threadStart1 = new ThreadStart(DoWork);

            // Создать объект thread обертывающий (wrap) threadStart1.
            Thread workThread = new Thread(threadStart1);

            // Вызвать start thread
            workThread.Start();

            // Создать объект Programmer.
            Programmer tran = new Programmer("Tran");

            // Вы так же можете создать объект ThreadStart обертывающий нестатический метод.
            // (ThreadStart только может обернуть методы без параметра)
            ThreadStart threadStart2 = new ThreadStart(tran.DoCode);

            // Создать Thread обертывающий threadStart2.
            Thread progThread = new Thread(threadStart2);

            progThread.Start();

            Console.WriteLine("Main thread ends");
            Console.Read();
        }


        public static void DoWork()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.Write("*");
                Thread.Sleep(100);
            }                
        }
    }

}
Запуск примера:

5- Thread с анонимными кодами

Выше вы создали потоки  Thread используя определенный метод. Вы можете создать поток чтобы выполнить любой код.
// Использовать delegate() чтобы создать анонимный метод.
delegate()
{
     //  ...
}
Пример:
ThreadUsingSnippetCode.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class ThreadUsingSnippetCode
    {

        public static void Main(string[] args) 
        {

            Console.WriteLine("Create thread 1");

            // Создать thread для выполнения кода.
            Thread newThread1 = new Thread(
                delegate()
                {
                    for (int i = 0; i < 10; i++)
                    {
                        Console.WriteLine("Code in delegate() " + i);
                        Thread.Sleep(50);
                    }

                }
            );

            Console.WriteLine("Start newThread1");

            
            // Start thread. 
            newThread1.Start();

            Console.WriteLine("Create thread 2");

            // Создать thread для выполнения кода.
            Thread newThread2 = new Thread(
                delegate(object value)
                {
                    for (int i = 0; i < 10; i++)
                    {
                        Console.WriteLine("Code in delegate(object) " + i + " - " + value);
                        Thread.Sleep(100);
                    }

                }
            );

            Console.WriteLine("Start newThread2");

            // Начать thread 2.
            // Передать значение в delegate().
            newThread2.Start("!!!");


            Console.WriteLine("Main thread ends");
            Console.Read();

        }
    }

}
Запуск примера:

6- Назовите Thread

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

В одном потоке thread вы можете назвать  Thread.CurrentThread.Name чтобы получить имя потока, выполняющего на тот момент.

Пример по иллюстрации:
NamingThreadDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class NamingThreadDemo
    {

        public static void Main(string[] args)
        {
            // Настроить название текущему thread 
            // (Является главным thread).
            Thread.CurrentThread.Name = "Main";

            Console.WriteLine("Code of "+ Thread.CurrentThread.Name);

            Console.WriteLine("Create new thread");

            // Создать thread.
            Thread letgoThread = new Thread(LetGo);

            // Дать название данному thread.
            letgoThread.Name = "Let's Go";

            letgoThread.Start();

            for (int i = 0; i < 5; i++)
            {
                Console.WriteLine("Code of " + Thread.CurrentThread.Name);
                Thread.Sleep(30);
            }

            Console.Read();
        }


        public static void LetGo()
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("Code of " + Thread.CurrentThread.Name);
                Thread.Sleep(50);
            }
        }
    }

}
Запуск примера:

7- Приоритет между Thread

В C# есть 5 уровней приоритета потока, они определяются в enum ThreadPriority.
** ThreadPriority enum **
enum ThreadPriority {
    Lowest,
    BelowNormal,
    Normal,
    AboveNormal,
    Highest
}
Обычно с выскоскоростным компьютером, если потоки выполняют малую работу, вам очень сложно определить разницу между потоками выского приоритета и низкого приоритета.

Пример ниже имеет 2 потока, каждый поток печатает 100 тысяч строк (Достаточно большое количество, чтобы увидеть разницу). 
ThreadPriorityDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class ThreadPriorityDemo
    {
        private static DateTime endsDateTime1;
        private static DateTime endsDateTime2;


        public static void Main(string[] args)
        {
            endsDateTime1 = DateTime.Now;
            endsDateTime2 = DateTime.Now;

            Thread thread1 = new Thread(Hello1);

            // Настроить самый высокий приоритет для thread1
            thread1.Priority = ThreadPriority.Highest;

            Thread thread2 = new Thread(Hello2);

            // Настроить самый низкий приоритет для thread2.
            thread2.Priority = ThreadPriority.Lowest;


            thread2.Start(); thread1.Start();

            Console.Read();
        }


        public static void Hello1()
        {
            for (int i = 0; i < 100000; i++)
            {
                Console.WriteLine("Hello from thread 1: "+ i);
            }
            // Момент thread1 завершается.
            endsDateTime1 = DateTime.Now;

            PrintInterval();
        }

        public static void Hello2()
        {
            for (int i = 0; i < 100000; i++)
            {
                Console.WriteLine("Hello from thread 2: "+ i);
            }
            // Момент thread2 завершается
            endsDateTime2 = DateTime.Now;

            PrintInterval();
        }

        private static void PrintInterval()
        {
            // Период времени (Милисекунды)
            TimeSpan interval = endsDateTime2 - endsDateTime1;

 
            Console.WriteLine("Thread2 - Thread1 = " + interval.TotalMilliseconds + " milliseconds");
        }
    }

}
Запуск примера:

8- Использование Join()

Thread.Join() это метод оповещения об ожидании завершении данного потока перед тем, как продолжить запуск главный поток.
ThreadJoinDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;


namespace MultithreadingTutorial
{
    class ThreadJoinDemo
    {

        public static void Main(string[] args)
        {
            Console.WriteLine("Create new thread");

            Thread letgoThread = new Thread(LetGo); 
            
            // Начать Thread (start thread).
            letgoThread.Start();

            // Сказать родительскому thread (здесь будет Main thread)
            // подождите когда letgoThread завершится потом продолжайте запуск.
            letgoThread.Join();
            
            // Данный код должен подождать когда letgoThread завершится, и потом запустить.
            Console.WriteLine("Main thread ends");
            Console.Read();
        }


        public static void LetGo()
        {
            for (int i = 0; i < 15; i++)
            {
                Console.WriteLine("Let's Go " + i);
            }
        }
    }

}
Запуск примера:

9- Использование Yield()

Теоритически, "Yield" означает отпустить, сдаться. Поток  Yield говорит системе, что он готов дать другим потокам потокам встать на свои места. Это означает что поток не делает что-то критически важное. Заметьте, что это подсказка, несмотря на то, что нет гарантии эффекта. 
Так что метод  Yield()  используется когда вы видите что поток свободен, он не делает ничего важного, он подсказывает операционной системе дать временный приоритет другим потокам.
В следующем примере, имеется 2 потока, каждый поток печатает одну строку 100 тысяч раз (достаточно большое количество, чтобы увидеть разницу). Одному потоку дан самый высокий уровень приоритета и другому самый низкий уровень приоритета. Измерить время окончания 2-х потоков.
ThreadYieldDemo.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Threading;

namespace MultithreadingTutorial
{
    class ThreadYieldDemo
    {

        private static DateTime importantEndTime;
        private static DateTime unImportantEndTime;

        public static void Main(string[] args)
        {
            importantEndTime = DateTime.Now;
            unImportantEndTime = DateTime.Now;

            Console.WriteLine("Create thread 1");

            Thread importantThread = new Thread(ImportantWork);

            // Настроить самый высокий приоритет для данного потока.
            importantThread.Priority = ThreadPriority.Highest; 

            Console.WriteLine("Create thread 2");

            Thread unImportantThread = new Thread(UnImportantWork);

            // Настроить самый низкий приоритет для данного потока.
            unImportantThread.Priority = ThreadPriority.Lowest; 

            // Start threads. 
            unImportantThread.Start();
            importantThread.Start();
           

            Console.Read();

        }

        // Важное дело, требует высокий приоритет.
        public static void ImportantWork()
        {
            for (int i = 0; i < 100000; i++)
            {
                Console.WriteLine("\n Important work " + i); 

                // Объявить операционной системе, данный поток
                // уступить уровень приоритета для других потоков.
                Thread.Yield();
            }
            // Момент данный thread завершается.
            importantEndTime = DateTime.Now;
            PrintTime();
        }

        public static void UnImportantWork()
        {
            for (int i = 0; i < 100000; i++)
            {
                Console.WriteLine("\n  -- UnImportant work " + i); 
            }
            // Момент данный thread завершается.
            unImportantEndTime = DateTime.Now;
            PrintTime();
        }

        private static void PrintTime()
        { 
            // Период времени (Милисекунды)
            TimeSpan interval = unImportantEndTime - importantEndTime; 
      
            Console.WriteLine("UnImportant Thread - Important Thread = " + interval.TotalMilliseconds +" milliseconds");
        }
         
    }

}
Запуск класса в случае отсутствия  Thread.Yield():
Запуск класса в случае поток с высоким приоритетом постоянно вызывает  Thread.Yield() чтобы потребовать систему временном дать приоритет другому потоку.

10- TODO