Поручаем заезд другому потоку
В прошлых уроках наш заезд шёл строго по очереди. Мы вызывали car.Drive(3) в foreach, и каждая машина заканчивала все круги, прежде чем начиналась следующая. Это последовательность: Main дожидался возврата из метода и только потом переходил к следующему вызову.
Теперь попробуем иначе. Мы по-прежнему хотим, чтобы машины ехали, но не будем заставлять Main ждать каждую. Вместо этого поручим каждую машину отдельному потоку — и тогда Main сможет сразу продолжить работу, а машины поедут своим ходом.
Вот полный файл Program.cs. Он заменяет всё, что было раньше; using System.Threading нужен для работы с потоками.
using System;
using System.Threading;
class RaceCar
{
public string Name { get; }
private int lapDelayMs;
public RaceCar(string name, int lapDelayMs)
{
Name = name;
this.lapDelayMs = lapDelayMs;
}
public void Drive(int totalLaps)
{
for (int lap = 1; lap <= totalLaps; lap++)
{
Console.WriteLine($"{Name}: круг {lap}");
Thread.Sleep(lapDelayMs);
}
Console.WriteLine($"{Name}: финиш!");
}
}
class Program
{
static void Main()
{
RaceCar[] cars =
{
new RaceCar("Молния", 300),
new RaceCar("Комета", 450),
new RaceCar("Вихрь", 600)
};
foreach (RaceCar car in cars)
{
Thread thread = new Thread(() => car.Drive(3));
thread.Start();
}
Console.WriteLine("Все автомобили получили команду старта");
}
}
Здесь появились две новые вещи: Thread и Start. Разберём их по порядку.
new Thread(...) создаёт объект потока и подготавливает его к запуску — но ещё ничего не выполняет. В скобках мы передаём действие, которое этот поток должен будет выполнить. Действие записано как () => car.Drive(3) — это лямбда, короткая запись маленького метода без имени. Обратите внимание: car.Drive(3) здесь не вызывается. Если написать car.Drive(3); отдельной командой, она выполнится сразу в текущем потоке. В записи () => car.Drive(3) вызов находится в теле лямбды: мы передаём её как действие, которое ещё предстоит вызвать. Внутри new Thread мы описываем, что нужно сделать позже.
Запускает подготовленный поток Start. После него действие начинает выполняться в новом потоке, а Main продолжает со следующей строки — не дожидаясь, пока машина проедет круги.
Thread.Sleep(lapDelayMs) внутри Drive — учебная пауза. Она приостанавливает текущий поток на заданный промежуток времени; фактическое возобновление может произойти позже из-за планирования. Приостанавливается именно текущий поток: тот, в котором выполняется этот Drive, а не все потоки программы. Это не физика и не способ согласовать работу потоков — просто задержка, чтобы события в консоли были видны. Молния едет с паузой 300 миллисекунд между кругами, Комета — 450, Вихрь — 600.
Теперь посмотрим на возможный журнал:
Молния: круг 1
Все автомобили получили команду старта
Комета: круг 1
Вихрь: круг 1
Молния: круг 2
Комета: круг 2
Молния: круг 3
Вихрь: круг 2
Молния: финиш!
...
Порядок здесь не фиксирован. Строки разных машин перемешиваются, сообщение Main может оказаться в самом начале, а может — между кругами. Мы не можем заранее сказать, кто поедет первым: планировщик решает это сам, и от запуска к запуску картина может меняться. Поэтому обещать строгий порядок кругов было бы неверно.
Обратите внимание: сообщение в конце Main — это не «Заезд завершён». Мы пока не проверили, завершили ли машины свои заезды. Надпись «Все автомобили получили команду старта» описывает выполненное действие: мы раздали команды, а что будет дальше — дело потоков.
Есть и ещё одна деталь. Мы создаём потоки в цикле: массив Thread[] не нужен, потому что каждый поток мы запускаем сразу. Если бы нам понадобилось хранить их для дальнейшей работы — например, чтобы дождаться завершения, — массив пригодился бы. Это нас ждёт в следующем уроке.
Что нужно знать про Start и завершение Main
Start можно вызвать только один раз. После запуска поток выполняет своё действие и завершается, но повторно его запустить нельзя — даже если он уже закончил работу. Для нового запуска нужен новый объект Thread.
Ещё одна особенность: по умолчанию потоки, созданные через new Thread, считаются foreground. Пока есть хотя бы один такой незавершённый поток, программа не выгружается, даже если Main уже дошёл до конца. Именно поэтому наш заезд не обрывается: Main завершается быстро, а машины продолжают ехать. Подробно разберём это позже — пока просто знайте, что заезд не пропадёт.
Мы получили настоящую конкурентность: несколько потоков продвигают свои работы в общий промежуток времени. Кто именно получит процессор в следующий момент — решает планировщик. В следующем уроке мы научимся дожидаться финиша: посмотрим, как Main может подождать завершения потоков и только потом объявить заезд законченным.
Мы изменили заезд: вместо последовательного car.Drive(3) каждая машина теперь получает отдельный поток. new Thread подготавливает поток к работе и принимает действие — в нашем случае лямбду () => car.Drive(3). Важно, что метод при этом не вызывается: он будет выполнен после Start. Start запускает поток, а Main продолжает работу, не дожидаясь машины.
Общий порядок сообщений не гарантирован, хотя круги каждой отдельной машины идут от первого к третьему. Сообщение Main может появиться до или после разных автомобильных событий. Так работает планировщик: он решает, какой поток получит процессор в следующий момент. Thread.Sleep внутри Drive — учебная пауза, которая останавливает текущий поток на заданное время, чтобы события были заметны.
Start можно вызвать один раз: завершившийся поток повторно не запускается, нужен новый Thread. Потоки, созданные через new Thread, по умолчанию foreground, поэтому завершение Main не обрывает заезд. В следующем уроке мы дождёмся финиша машин и объявим заезд завершённым.