Во введении к циклу статей про Многопоточность в C# мы создали простое приложение, демонстрирующее работу двух потоков, не вдаваясь особенно в то, что мы написали. Сегодня мы более подробнее посмотрим на конструкторы класса Thread
, а именно — рассмотрим делегаты, используемые при создании потоков.
Делегат ThreadStart
Итак, в предыдущей части мы использовали вот такой конструктор создания потока:
Thread thread = new Thread(new ThreadStart(Proc));
Здесь ThreadStart
— это ни что иное, как делегат (о делегатах мы говорили здесь), который сообщает нам, что метод не должен ничего принимать и ничего возвращать. Описание такого делегата выглядит следующим образом:
public delegate void ThreadStart();
Например, мы можем написать вот такое приложение, используя для потока делегат ThreadStart
:
Thread thread = new Thread(new ThreadStart(Proc)); thread.Start(); for (int i = 0; i < 10; i++) { Console.WriteLine($"Значение из главного потока: {i * i * i}"); Thread.Sleep(500); } static void Proc() { for (int i = 0; i < 10; i++) { Console.WriteLine($"Значение из второго потока: {i * i}"); Thread.Sleep(1000); } }
Второй поток будет выполнять метод Proc
, который мы передали в качестве параметра делегату ThreadStart
. Так как вторичный поток не определен как фоновый (свойство IsBackground
по умолчанию равно false
), то в консоли мы увидим примерно следующие результаты:
Значение из второго потока: 0
Значение из главного потока: 1
Значение из второго потока: 1
Значение из главного потока: 8
Значение из главного потока: 27
Значение из второго потока: 4
Значение из главного потока: 64
Значение из главного потока: 125
Значение из второго потока: 9
Значение из главного потока: 216
Значение из главного потока: 343
Значение из второго потока: 16
Значение из главного потока: 512
Значение из главного потока: 729
Значение из второго потока: 25
Значение из второго потока: 36
Значение из второго потока: 49
Значение из второго потока: 64
Значение из второго потока: 81
Делегат ParameterizedThreadStart
Если нам необходимо передать какие-либо параметры в поток, например, начальные значения переменных, то мы можем воспользоваться вторым делегатом — ParameterizedThreadStart
. Выглядит этот делегат следующим образом:
public delegate void ParameterizedThreadStart(object? obj);
то есть, метод, передаваемый в параметре делегата не должен ничего возвращать, но может принимать один параметр типа object
. А так как мы помним, что все в C# так или иначе является объектами, то подобный подход позволяет нам передать в поток вообще всё, что угодно — от простого числа до сложный объектов. В качестве демонстрации использования этого делегата, воспользуемся алгоритмом поиска простых чисел и напишем приложение в котором вторичный поток будет определять простые числа, а главный — выполнять ИБД (Имитацию Бурной Деятельности):
int N = 10000; Thread thread = new Thread(new ParameterizedThreadStart(Proc)); thread.Start(N);//передаем начальные настройки потока for (int i = 0; i < 10; i++) { Console.WriteLine($"Имитируем бурную деятельность главного потока"); Thread.Sleep(1000); } static bool IsPrime(int number) { for (int i = 2; i < number; i++) { if (number % i == 0) return false; } return true; } static void Proc(object N) { int n = (int)N; for (int i = 1; i <= n; i++) { if (IsPrime(i)) { Console.WriteLine($"{i} - простое число"); Thread.Sleep(50); } } }
Здесь стоит обратить внимание на то, как мы создаем и запускаем вторичный поток. В качестве параметра в конструкторе Thread
мы используем делегат ParameterizedThreadStart
в который передаем метод Proc
. Метод Proc
, как и требуется, принимает всего один параметр типа object
, поэтому мы приводим параметр N
к нужному нам типу — int
. Чтобы указать конкретное значение, которое мы хотим передать в поток, мы используем перегруженную версию метода Start
и в его параметре передаем необходимое значение в поток (в нашем случае — это значение 10000
).
Конечно, такая работа с потоками не безопасна, как минимум по причине того, что мы могли бы передать в метод Start
не число, а например, какой-то сложный объект или строку и тогда мы бы получили исключение. Избежать этого мы можем относительно просто — объявить специальный класс с необходимым нам методом без параметров и воспользоваться делегатом ThreadStart
. Перепишем наше приложение следующим образом:
PrimeCounter primeCounter = new PrimeCounter(); primeCounter.N = 10000; Thread thread = new Thread(new ThreadStart(primeCounter.Calculate)); thread.Start(); for (int i = 0; i < 10; i++) { Console.WriteLine($"Имирируем бурную деятельность главного потока"); Thread.Sleep(1000); } public class PrimeCounter { public int N { get; set; } private bool IsPrime(int number) { for (int i = 2; i < number; i++) { if (number % i == 0) return false; } return true; } public void Calculate() { for (int i = 1; i <= N; i++) { if (IsPrime(i)) { Console.WriteLine($"{i} - простое число"); Thread.Sleep(50); } } } }
Обратите внимание на то, что все необходимые методы, в том числе и метод IsPrime
вынесены в отдельный класс. Теперь наш код стал типобезопасным — мы уже не сможем передать классу в качестве значения свойства N
строку. В потоке же мы воспользовались делегатом ThreadStart
и передали ему метод Calculate
класса PrimeCounter
. При этом, результат работы программы никак не изменился.
Итого
Мы рассмотрели два делегата, используемых при создании потоков в C# — ThreadStart
и ParameterizedThreadStart
. Использование делегата ParameterizedThreadStart
хоть и позволяет передавать в поток какую-либо информацию, однако использование этого делегата может быть сопряжено с проблемами безопасности, так как в поток может передаваться любое значение типа object
. Более безопасно, с этой точки зрения, оформить все необходимые для потока операции в виде отдельного класса, в котором определить метод без параметров и передать его в качестве параметра для делегата ThreadStart
в конструкторе потока Thread
.