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

using System;

class Program
{
    static void PrintWords()
    {
        string[] words = { "apple", "banana", "cherry" };
        Console.WriteLine("Перебираем слова:");
        foreach (var word in words)
            Console.WriteLine($"  {word}");
    }

    static void CalculateSquares()
    {
        Console.WriteLine("Считаем квадраты:");
        for (int i = 1; i <= 5; i++)
            Console.WriteLine($"  {i}^2 = {i * i}");
    }

    static void Main()
    {
        PrintWords();
        CalculateSquares();
        Console.WriteLine("Готово.");
    }
}

Запустите — и увидите ровно то, что ожидаете. Сначала все слова, потом все квадраты, потом «Готово». Строго по порядку. Второй метод не начнёт работу, пока первый не закончит. Это однопоточное выполнение — предсказуемое и скучное.

А теперь давайте заставим их работать одновременно. В C# поток — это объект класса Thread из пространства имён System.Threading. Это может показаться странным: мы привыкли, что объекты представляют «вещи» — игрока, заказ, файл. А тут объект представляет последовательность действий, которая выполняется параллельно с другими. Непривычно, но работает ровно так же: создаём объект, вызываем методы.

Создание потока — два шага: передать метод в конструктор, вызвать Start():

Thread thread = new Thread(PrintWords); // Создали — поток знает ЧТО делать
thread.Start();                          // Запустили — поток начал работу

Между созданием и запуском поток существует, но ничего не делает — он в состоянии «Unstarted». Только после Start() он начнёт выполнять переданный метод.

Перепишем программу с двумя потоками:

using System;
using System.Threading;

class Program
{
    static void PrintWords()
    {
        string[] words = { "apple", "banana", "cherry" };
        Console.WriteLine("Перебираем слова:");
        foreach (var word in words)
        {
            Console.WriteLine($"  [Слова] {word}");
            Thread.Sleep(100); // Небольшая пауза для наглядности
        }
    }

    static void CalculateSquares()
    {
        Console.WriteLine("Считаем квадраты:");
        for (int i = 1; i <= 5; i++)
        {
            Console.WriteLine($"  [Квадраты] {i}^2 = {i * i}");
            Thread.Sleep(100);
        }
    }

    static void Main()
    {
        Thread wordsThread = new Thread(PrintWords);
        Thread squaresThread = new Thread(CalculateSquares);

        wordsThread.Start();
        squaresThread.Start();

        Console.WriteLine("Main: оба потока запущены.");
    }
}

Запустите этот код. А теперь — запустите ещё раз. И ещё раз.

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

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

Планировщик смотрит на всю эту очередь и раздаёт процессорное время маленькими порциями — квантами. Каждый поток получает свой квант (обычно 15–30 миллисекунд), работает, а потом уступает место следующему. Потом снова встаёт в очередь, снова получает квант, и так далее.

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

Именно поэтому:

— Вы не можете гарантировать, какой поток выполнится первым.

— Вы не можете гарантировать порядок строк в выводе.

— Вы не можете «занять» весь процессор для своей программы.

Это фундаментальное свойство многопоточности, и к нему нужно привыкнуть. Код, который зависит от конкретного порядка выполнения потоков, — это бомба замедленного действия.

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

При этом программа не завершится, пока потоки работают — потому что они foreground (приоритетные) по умолчанию. Процесс ждёт все foreground-потоки. Но основной поток не знает, когда именно они закончат. Он не может сказать: «выведи это сообщение строго после обоих потоков». Строка Console.WriteLine("Готово") выполнится сразу, а не после завершения потоков.

Как заставить основной поток дождаться конкретного момента завершения — узнаем в следующем уроке, когда познакомимся с методом Join(). Кстати, у каждого потока есть свойства, которые можно прочитать. Давайте заглянем в текущий поток:

Thread current = Thread.CurrentThread;

Console.WriteLine($"Имя: {current.Name ?? "не задано"}");
Console.WriteLine($"ID: {current.ManagedThreadId}");
Console.WriteLine($"Фоновый: {current.IsBackground}");
Console.WriteLine($"Приоритет: {current.Priority}");

Thread.CurrentThread — статическое свойство, которое возвращает объект текущего потока. Через него можно узнать имя, ID, приоритет и другие характеристики. Это бывает полезно при отладке — чтобы понять, в каком потоке выполняется конкретный кусок кода.

Thread — класс из System.Threading. В конструктор принимает метод, Start() запускает выполнение в отдельном потоке.

Порядок выполнения потоков непредсказуем — им управляет планировщик ОС, а не ваш код.

Thread.CurrentThread — статическое свойство, возвращающее объект текущего потока (имя, ID, приоритет).

Thread.Sleep(ms) — приостанавливает текущий поток на указанное количество миллисекунд.

Один объект Thread можно запустить только один раз. Повторный Start() — исключение.

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

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

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

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