Создание вторичных потоков

78

При программном создании дополнительных потоков для выполнения некоторой единицы работы необходимо следовать строго регламентированному процессу:

Согласно второму шагу, можно использовать два разных типа делегатов для "указания" метода, который выполнит вторичный поток. Делегат ThreadStart относится к пространству имен System.Threading, начиная с .NET 1.0, и он может указывать на любой метод, не принимающий аргументов и ничего не возвращающий. Этот делегат пригодится, когда метод предназначен просто для запуска в фоновом режиме, без какого-либо дальнейшего взаимодействия.

Очевидное ограничение ThreadStart связано с невозможность передавать ему параметры для обработки. Тем не менее, тип делегата ParametrizedThreadStart позволяет передать единственный параметр типа System.Object. Учитывая, что с помощью System.Object представляется все, что угодно, можно передать любое количество параметров через специальный класс или структуру. Однако имейте в виду, что делегат ParametrizedThreadStart может указывать только на методы, возвращающие void.

Делегат ThreadStart

Чтобы проиллюстрировать процесс построения многопоточного приложения (а также его пользу), предположим, что есть консольное приложение, которое позволяет конечному пользователю выбирать, будет приложение выполнять свою работу в единственном первичном потоке либо распределит рабочую нагрузку на два отдельных потока выполнения.

После импортирования пространства имен System.Threading следующий шаг заключается в определении метода для выполнения работы (возможного) вторичного потока. Чтобы сосредоточиться на механизме построения многопоточных программ, этот метод будет просто выводить на консоль последовательность чисел, приостанавливаясь примерно на 2 секунды на каждом шаге. Ниже показано полное определение класса:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Windows;

namespace ConsoleApplication1
{
    public class MyThread
    {
        public void ThreadNumbers()
        {
            // Информация о потоке
            Console.WriteLine("{0} поток использует метод ThreadNumbers",Thread.CurrentThread.Name);
            // Выводим числа
            Console.Write("Числа: ");
            for (int i = 0; i < 10; i++)
            {
                Console.Write(i+", ");
                // Используем задержку
                Thread.Sleep(3000);
            }
            Console.WriteLine();
        }
    }

    class Program
    {
        static void Main()
        {
            Console.Write("Сколько использовать потоков (1 или 2)?");
            string number = Console.ReadLine();

            Thread mythread = Thread.CurrentThread;
            mythread.Name = "Первичный";

            // Выводим информацию о потоке
            Console.WriteLine("--> {0} главный поток", Thread.CurrentThread.Name);
            MyThread mt = new MyThread();

            switch (number)
            {
                case "1":
                    mt.ThreadNumbers();
                    break;
                case "2":
                    // Создаем поток
                    Thread backgroundThread = new Thread(new ThreadStart(mt.ThreadNumbers));
                    backgroundThread.Name = "Вторичный";
                    backgroundThread.Start();
                    break;
                default:
                    Console.WriteLine("использую 1 поток");
                    goto case "1";
            }
            MessageBox.Show("Сообщение ...","Работа в другом потоке");
            Console.ReadLine();            
        }
    }
}

Внутри Main() сначала пользователю предлагается решить, сколько потоков применять для выполнения работы приложения: один или два. Если пользователь запрашивает один поток, нужно просто вызвать метод ThreadNumbers() внутри первичного потока. Если же пользователь отдает предпочтение двум потокам, необходимо создать делегат ThreadStart, указывающий на ThreadNumbers(), передать объект делегата конструктору нового объекта Thread и вызвать метод Start(), информируя среду CLR, что этот поток готов к обработке.

Если теперь запустить эту программу в одном потоке, обнаружится, что финальное окно сообщения не отображает сообщения, пока вся последовательность чисел не будет выведена на консоль. Поскольку после вывода каждого числа установлена пауза примерно в 2 секунды, это создаст не слишком приятное впечатление у пользователя. Однако в случае выбора двух потоков окно сообщения отображается немедленно, поскольку для вывода чисел на консоль выделен отдельный уникальный объект Thread:

Пример многопоточного приложения

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

Делегат ParametrizedThreadStart

Вспомните, что делегат ThreadStart может указывать только на методы, возвращающие void и не имеющие аргументов. Во многих случаях это подходит, но если нужно передать данные методу, выполняющемуся во вторичном потоке, то придется использовать тип делегата ParametrizedThreadStart.

Давайте рассмотрим пример:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;

namespace ConsoleApplication1
{
    public class Params
    {
        public int a, b;
        public Params(int a, int b)
        {
            this.a = a;
            this.b = b;
        }
    }

    class Program
    {
        static void Add(object obj)
        {
            if (obj is Params)
            {
                Console.WriteLine("ID потока метода Add(): " + Thread.CurrentThread.ManagedThreadId);
                Params pr = (Params)obj;
                Console.WriteLine("{0} + {1} = {2}",pr.a,pr.b,pr.a+pr.b);
            }
        }

        static void Main()
        {
            Console.WriteLine("Главный поток. ID: " + Thread.CurrentThread.ManagedThreadId);

            Params pm = new Params(10, 10);
            Thread t = new Thread(new ParameterizedThreadStart(Add));
            t.Start(pm);

            // Задержка
            Thread.Sleep(5);
            Console.ReadLine();
        }
    }
}

Класс AutoResetEvent

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

Более безопасной, хотя также нежелательной альтернативой может быть вызов Thread.Sleep() на определенный период времени. Проблема в том, что нет желания ждать больше, чем необходимо.

Простой и безопасный к потокам способ заставить один поток ожидать завершения другого потока, предусматривает использование класса AutoResetEvent. В потоке, который должен ждать (таком как поток метода Main()), создадим экземпляр этого класса и передадим конструктору false, указав, что уведомления пока не было. В точке, где требуется ожидать, вызовем метод WaitOne(). Ниже приведен измененный класс Program, который делает все это, используя статическую переменную-член AutoResetEvent:

class Program
{
        private static AutoResetEvent waitHandle = new AutoResetEvent(false);

        static void Main()
        {
            Console.WriteLine("Главный поток. ID: " + Thread.CurrentThread.ManagedThreadId);

            Params pm = new Params(10, 10);
            Thread t = new Thread(new ParameterizedThreadStart(Add));
            t.Start(pm);

            // Задержка
            // Thread.Sleep(5);

            // Ждем уведомления
            waitHandle.WaitOne();
            Console.WriteLine("Все потоки завершились");

            Console.ReadLine();
        }

        static void Add(object obj)
        {
            if (obj is Params)
            {
                Console.WriteLine("ID потока метода Add(): " + Thread.CurrentThread.ManagedThreadId);
                Params pr = (Params)obj;
                Console.WriteLine("{0} + {1} = {2}", pr.a, pr.b, pr.a + pr.b);

                // Сообщить другому потоку о завершении работы
                waitHandle.Set();
            }
        }
  }
Пройди тесты
Лучший чат для C# программистов