Console.WriteLine("This is on the main thread, and we are finished.");
Console.ReadLine;
Если теперь вы запустите программу с одним потоком, то обнаружите, что финальное окно сообщения не будет отображать сообщение, пока вся последовательность чисел не выведется на консоль. Поскольку после вывода каждого числа установлена пауза около 2 секунд, это создаст не особенно приятное впечатление у конечного пользователя. Однако в случае выбора двух потоков окно сообщения отображается немедленно, потому что для вывода чисел на консоль выделен отдельный объект
Thread
.
Работа
с делегатом ParametrizedThreadStart
Вспомните, что делегат
ThreadStart
может указывать только на методы, которые возвращают
void
и не принимают аргументов. В некоторых случаях это подходит, но если нужно передать данные методу, выполняющемуся во вторичном потоке, тогда придется использовать тип делегата
ParametrizedThreadStart
. В целях иллюстрации создайте новый проект консольного приложения по имени
AddWithThreads
и импортируйте пространство имен
System.Threading
. С учетом того, что делегат
ParametrizedThreadStart
может указывать на любой метод, принимающий параметр типа
System.Object
, постройте специальный тип, который содержит числа, подлежащие сложению:
namespace AddWithThreads
{
class AddParams
{
public int a, b;
public AddParams(int numb1, int numb2)
{
a = numb1;
b = numb2;
}
}
}
Далее создайте в классе
Program
статический метод, который принимает параметр
AddParams
и выводит на консоль сумму двух чисел:
void Add(object data)
{
if (data is AddParams ap)
{
Console.WriteLine("ID of thread in Add: {0}",
Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("{0} + {1} is {2}",
ap.a, ap.b, ap.a + ap.b);
}
}
Код в файле
Program.cs
прямолинеен. Вместо типа
ThreadStart
просто используется
ParametrizedThreadStart
:
using System;
using System.Threading;
using AddWithThreads;
Console.WriteLine("***** Adding with Thread objects *****");
Console.WriteLine("ID of thread in Main: {0}",
Thread.CurrentThread.ManagedThreadId);
// Создать объект AddParams для передачи вторичному потоку.
AddParams ap = new AddParams(10, 10);
Thread t = new Thread(new ParameterizedThreadStart(Add));
t.Start(ap);
// Подождать, пока другой поток завершится.
Thread.Sleep(5);
Console.ReadLine;
Класс AutoResetEvent
В приведенных выше начальных примерах нет какого-либо надежного способа узнать, когда вторичный поток завершит свою работу. В последнем примере метод
Sleep
вызывался с произвольным временным периодом, чтобы дать возможность другому потоку завершиться. Простой и безопасный к потокам способ заставить один поток ожидать, пока не завершится другой поток, предусматривает применение класса
AutoResetEvent
. В потоке, который должен ожидать, создайте экземпляр
AutoResetEvent
и передайте его конструктору значение
false
, указав, что уведомления пока не было. Затем в точке, где требуется ожидать, вызовите метод
WaitOne
. Ниже приведен модифицированный класс
Program
, который делает все описанное с использованием статической переменной-члена
AutoResetEvent
:
AutoResetEvent _waitHandle = new AutoResetEvent(false);
Console.WriteLine("***** Adding with Thread objects *****");
Console.WriteLine("ID of thread in Main: {0}",
Thread.CurrentThread.ManagedThreadId);
AddParams ap = new AddParams(10, 10);
Thread t = new Thread(new ParameterizedThreadStart(Add));
t.Start(ap);
// Ожидать, пока не поступит уведомление!
_waitHandle.WaitOne;
Console.WriteLine("Other thread is done!");
Console.ReadLine;
...
Когда другой поток завершит свою работу, он вызовет метод
Set
на том же самом экземпляре типа
AutoResetEvent
:
void Add(object data)
{
if (data is AddParams ap)
{
Console.WriteLine("ID of thread in Add: {0}",
Thread.CurrentThread.ManagedThreadId);
Console.WriteLine("{0} + {1} is {2}",
ap.a, ap.b, ap.a + ap.b);
// Сообщить другому потоку о том, что работа завершена.
_waitHandle.Set;
}
}
Потоки переднего плана и фоновые потоки
Теперь, когда вы знаете, как программно создавать новые потоки выполнения с применением типов из пространства имен
System.Threading
, давайте формализуем разницу между потоками переднего плана и фоновыми потоками.
• Потоки переднего плана имеют возможность предохранять текущее приложение от завершения. Среда .NET Core Runtime не будет прекращать работу приложения (скажем, выгружая обслуживающий домен приложения) до тех пор, пока не будут завершены все потоки переднего плана.
• Фоновые потоки (иногда называемые потоками-демонами) воспринимаются средой .NET Core Runtime как расширяемые пути выполнения, которые в любой момент времени могут быть проигнорированы (даже если они заняты выполнением некоторой части работы). Таким образом, если при выгрузке домена приложения все потоки переднего плана завершены, то все фоновые потоки автоматически уничтожаются.
Важно отметить, что потоки переднего плана и фоновые потоки — не синонимы первичных и рабочих потоков. По умолчанию каждый поток, создаваемый посредством метода
Thread.Start
, автоматически становится потоком переднего плана. В итоге домен приложения не выгрузится до тех пор, пока все потоки выполнения не завершат свои единицы работы. В большинстве случаев именно такое поведение и требуется.