Наша гоночная трасса готова, автомобили тоже, но старт по-прежнему больше похож на пропускной пункт. Сначала все три круга проходит «Молния», затем на трассу выпускают «Комету» и только после неё — «Вихрь».

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

Для начала вынесем один заезд в отдельный поток.

Ручная работа с потоками в C# начинается с класса Thread. Он находится в пространстве имён System.Threading, поэтому в начало файла нужно добавить ещё один using:

using System.Threading;

Теперь создадим поток для «Молнии»:

RaceCar lightning = new RaceCar("Молния", 650);

Thread lightningThread = new Thread(() => lightning.Drive(3));

В этой строке сошлись две разные вещи. lightning.Drive(3) — работа, которую нужно выполнить, а объект Thread — отдельная последовательность выполнения, которой мы эту работу поручаем. Лямбда () => lightning.Drive(3) описывает действие, но пока его не запускает.

После new Thread(...) объект уже существует, но метод Drive ещё не вызван. Это полезное разделение: мы можем сначала подготовить все потоки, а команду на старт дать позже.

Командой служит метод Start:

lightningThread.Start();

Здесь важно слово «может». Вызов Start не означает, что первая команда нового потока выполнится прямо в ту же наносекунду. Основной поток сообщает о готовности, а дальше планировщик операционной системы решает, когда и на каком логическом процессоре дать новому потоку время.

И ещё один момент: Start не переносит основной поток внутрь Drive. После вызова основной поток идёт к следующей строке Main, а lightningThread начинает собственный путь по тому же процессу.

Делаем движение заметным

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

Добавим автомобилю время прохождения круга:

class RaceCar
{
    public string Name { get; }
    public int LapTimeMs { get; }

    public RaceCar(string name, int lapTimeMs)
    {
        Name = name;
        LapTimeMs = lapTimeMs;
    }

    public void Drive(int totalLaps)
    {
        Console.WriteLine($"{Name} выходит на трассу.");

        for (int lap = 1; lap <= totalLaps; lap++)
        {
            Thread.Sleep(LapTimeMs);
            Console.WriteLine($"{Name} завершает круг {lap}.");
        }

        Console.WriteLine($"{Name} финиширует.");
    }
}

Thread.Sleep приостанавливает именно тот поток, который его вызвал. Пока поток автомобиля ждёт окончания своей условной задержки, планировщик может выполнять другие готовые потоки.

Число передаётся в миллисекундах. При этом Thread.Sleep(650) не является точным секундомером: поток не продолжит работу раньше указанной задержки, но может продолжить немного позже, когда планировщик снова выделит ему процессорное время.

Выпускаем на трассу три автомобиля

Соберём обновлённый симулятор целиком:

using System;
using System.Threading;

class RaceCar
{
    public string Name { get; }
    public int LapTimeMs { get; }

    public RaceCar(string name, int lapTimeMs)
    {
        Name = name;
        LapTimeMs = lapTimeMs;
    }

    public void Drive(int totalLaps)
    {
        Console.WriteLine($"{Name} выходит на трассу.");

        for (int lap = 1; lap <= totalLaps; lap++)
        {
            Thread.Sleep(LapTimeMs);
            Console.WriteLine($"{Name} завершает круг {lap}.");
        }

        Console.WriteLine($"{Name} финиширует.");
    }
}

class Program
{
    static void Main()
    {
        RaceCar lightning = new RaceCar("Молния", 650);
        RaceCar comet = new RaceCar("Комета", 400);
        RaceCar whirlwind = new RaceCar("Вихрь", 850);

        Thread lightningThread =
            new Thread(() => lightning.Drive(3));
        Thread cometThread =
            new Thread(() => comet.Drive(3));
        Thread whirlwindThread =
            new Thread(() => whirlwind.Drive(3));

        lightningThread.Start();
        cometThread.Start();
        whirlwindThread.Start();

        Console.WriteLine("Все автомобили получили команду на старт.");
    }
}

Запустите программу несколько раз. Теперь круги разных автомобилей появляются вперемешку: пока «Вихрь» ждёт окончания своего более долгого круга, «Комета» может успеть сообщить о следующем. Скорее всего, она и финиширует раньше остальных, потому что её задержка меньше. Однако точный порядок первых сообщений всё равно может меняться от запуска к запуску.

Это и есть первое важное изменение нашего проекта. Раньше основной поток полностью проводил одну машину и только потом брался за следующую. Теперь после трёх вызовов Start внутри процесса работают четыре потока: основной и три автомобильных. Каждый автомобиль продвигается по своему вызову Drive, а планировщик решает, когда продолжить каждый из них.

Обратите внимание на сообщение основного потока:

Все автомобили получили команду на старт.

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

До этого за весь заезд отвечал основной поток: он входил в Drive, проводил один автомобиль по трём кругам и только после финиша переходил к следующему. Теперь у каждой машины может быть собственная последовательность выполнения. Объект Thread хранит действие, которое мы хотим поручить новому потоку, но само создание через new Thread(...) ещё ничего не запускает. Настоящий старт происходит только после вызова Start().

С этого момента основной и автомобильные потоки движутся независимо, а их очерёдность определяет планировщик. Поэтому нельзя заранее обещать, какая машина первой выведет сообщение и как перемешаются строки. Добавленный в Drive метод Thread.Sleep лишь делает это поведение заметнее: он блокирует текущий поток на время условного круга, но не превращает пример в доказательство ускорения.

У симулятора появилась настоящая многопоточность, а вместе с ней — новый вопрос. Основной поток умеет выпустить все машины на трассу, но пока совсем не умеет понять, когда заезд действительно закончился.

Обсуждение урока

0
Комментарии видны всем. Чтобы участвовать в обсуждении, войдите или зарегистрируйтесь.
Модерация сообщества

Пожаловаться на комментарий

Расскажите модераторам, что именно требует внимания.