Заезд, который идёт по очереди

Добро пожаловать в блок о многопоточности и асинхронности! Здесь мы будем собирать консольную гонку. Начнём с трёх автомобилей, а затем научимся выпускать их на общий старт, ограничивать въезд в тоннель и управлять заездом из диспетчерской. На трассе будет происходить сразу несколько событий, и нам предстоит разобраться, как организовать их в программе.

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

Создайте отдельный консольный проект ConsoleRace и замените весь текст Program.cs следующим листингом. Здесь мы явно объявляем Main — метод, с которого начнётся выполнение нашего кода. Команды, которые шаблон проекта разместил вне классов, нужно убрать: иначе именно они могут стать началом программы, а наш Main не будет вызван.

using System;

class RaceCar
{
    public string Name { get; }

    public RaceCar(string name)
    {
        Name = name;
    }

    public void Drive(int totalLaps)
    {
        for (int lap = 1; lap <= totalLaps; lap++)
        {
            Console.WriteLine($"{Name}: круг {lap}");
        }

        Console.WriteLine($"{Name}: финиш!");
    }
}

class Program
{
    static void Main()
    {
        RaceCar[] cars =
        {
            new RaceCar("Молния"),
            new RaceCar("Комета"),
            new RaceCar("Вихрь")
        };

        foreach (RaceCar car in cars)
        {
            car.Drive(3);
        }

        Console.WriteLine("Заезд завершён.");
    }
}

У RaceCar пока всего одна характеристика — имя. Метод Drive получает количество кругов, выводит сообщение о каждом из них и сообщает о финише. В Main мы создаём три автомобиля, собираем их в массив и вызываем один и тот же метод у каждого объекта. Все эти конструкции уже встречались нам в предыдущих блоках.

Сохраните файл и запустите проект сочетанием Ctrl+F5. В консоли появится такой журнал:

Молния: круг 1
Молния: круг 2
Молния: круг 3
Молния: финиш!
Комета: круг 1
Комета: круг 2
Комета: круг 3
Комета: финиш!
Вихрь: круг 1
Вихрь: круг 2
Вихрь: круг 3
Вихрь: финиш!
Заезд завершён.

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

Кто выполняет команды

Почему автомобили не продвигаются вместе? Потому что в нашем коде следующий вызов Drive начинается только после завершения предыдущего. Выполнение входит в метод, проходит весь цикл по кругам и возвращается в Main. Лишь после этого foreach выбирает следующую машину. Создание трёх объектов само по себе не запускает три независимые работы.

Чтобы понять, как изменить это поведение, посмотрим на запуск приложения. Когда мы запускаем наш симулятор, операционная система создаёт для него процесс: выделяет пространство памяти и другие ресурсы, необходимые программе. Внутри этого процесса находятся и наши объекты автомобилей.

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

Мы пока не создавали дополнительные потоки, но основной уже существует. Значит, знакомая программа тоже работает в потоке — просто нам до сих пор не требовалось управлять несколькими такими последовательностями.

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

Где выполняется поток? Его команды обрабатывает процессор. При этом поток и ядро процессора — разные вещи: ядро является частью оборудования, а поток описывает выполняемую работу. У созданного потока не появляется собственное выделенное ядро.

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

Планировщик распределяет потоки по логическим процессорам — доступным системе местам выполнения. Их число не обязательно совпадает с числом физических ядер: некоторые ядра поддерживают несколько аппаратных потоков. Для нас сейчас главное другое: число программных потоков не ограничено числом таких мест. Когда готовых потоков больше, часть из них получает время по очереди. При наличии нескольких доступных мест некоторые могут выполняться одновременно.

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

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

Мы собрали первую версию гонки и увидели поочерёдный заезд. Три объекта автомобиля уже существуют, но основной поток вызывает их методы один за другим. Пока Drive одной машины не вернёт управление, обход массива не перейдёт к следующей.

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

Теперь у нас есть рабочая основа курса: три автомобиля, три круга и понятный порядок сообщений. Дальше выясним, как несколько работ могут продвигаться в один период времени, а затем начнём менять запуск автомобилей.

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

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

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

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