Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
using System;
using System.Threading;
using SimpleMultiThreadApp;
Console.WriteLine("***** The Amazing Thread App *****n");
Console.Write("Do you want [1] or [2] threads? ");
string threadCount = Console.ReadLine(); // Запрос количества потоков
// Назначить имя текущему потоку.
Thread primaryThread = Thread.CurrentThread;
primaryThread.Name = "Primary";
// Вывести информацию о потоке.
Console.WriteLine("-> {0} is executing Main()",
Thread.CurrentThread.Name);
// Создать рабочий класс.
Printer p = new Printer();
switch(threadCount)
{
case "2":
// Создать поток.
Thread backgroundThread =
new Thread(new ThreadStart(p.PrintNumbers));
backgroundThread.Name = "Secondary";
backgroundThread.Start();
break;
case "1":
p.PrintNumbers();
break;
default:
Console.WriteLine("I don't know what you want...you get 1 thread.");
goto case "1"; // Переход к варианту с одним потоком
}
// Выполнить некоторую дополнительную работу.
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(), автоматически становится потоком переднего плана. В итоге домен приложения не выгрузится до тех пор, пока все потоки выполнения не завершат свои единицы работы.