JavaRush /Курсы /C# SELF /Запуск потоков с помощью класса

Запуск потоков с помощью класса Thread

C# SELF
55 уровень , 1 лекция
Открыта

1. Введение

Если представить процесс как супермаркет, то потоки (thread) — это кассиры на разных кассах, которые одновременно обслуживают покупателей. Все кассиры работают в одном магазине, но каждый из них выполняет свою задачу параллельно, благодаря чему работа идет быстрее и эффективнее. Потоки внутри процесса позволяют выполнять несколько задач одновременно, разделяя общие ресурсы и координируя работу внутри одного приложения. Главное преимущество — потоки действительно могут работать параллельно, если процессор поддерживает многозадачность.

Практическая необходимость потоков

Зачем нам вообще запускать несколько потоков? Вот лишь пара ситуаций из реальной жизни:

  • Вы пишете приложение с графическим интерфейсом и не хотите, чтобы оно "зависло" во время долгой операции.
  • Вам надо одновременно загружать несколько файлов.
  • В игре противники должны думать своей головой независимо от других.

Удивительно, но во многих программах по-прежнему встречаются "зависания" из-за неопытной работы с потоками. Сегодня мы научимся избегать таких недоразумений.

Класс Thread: основа ручной многопоточности

Класс Thread — динозавр среди многопоточного программирования в .NET. Несмотря на появление более современных инструментов (Task, async/await), работа с Thread по-прежнему имеет смысл, особенно если хочется почувствовать себя повелителем потоков "с чистого листа".

Схема создания потока

  1. Создать объект класса Thread, передав ему метод, который будет выполняться в потоке.
  2. Запустить поток с помощью метода Start().
  3. (Необязательно) Посмотреть, что происходит — ведь всё может работать параллельно!

2. Запуск потока с помощью Thread

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

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Console.WriteLine("Главный поток стартовал!");

        // Создаём объект потока, указывая на метод для выполнения
        Thread workerThread = new Thread(CountToTen);

        // Запускаем новый поток
        workerThread.Start();

        // Главный поток тоже что-то делает: пишет точки...
        for (int i = 0; i < 5; i++)
        {
            Console.Write(".");
            Thread.Sleep(500); // Задержка для наглядности
        }

        Console.WriteLine("\nГлавный поток завершён!");
    }

    static void CountToTen()
    {
        for (int i = 1; i <= 10; i++)
        {
            Console.WriteLine($"[Поток] Считаем: {i}");
            Thread.Sleep(400);
        }
        Console.WriteLine("[Поток] Готово!");
    }
}

Что происходит?

Вы увидите в консоли, что точки и "Считаем: X" появляются вперемешку. Это — первый признак многопоточности! Главный поток пишет свои точки, а новый поток считает до 10. Они друг другу не мешают, как два музыканта в группе: один играет на барабане, другой — на фортепиано. Каждый звучит по-своему, и вместе получается музыка.

3. Как передать данные в поток?

Иногда поток должен знать не только, что делать, но и с чем работать. Если метод для потока принимает параметры, как их туда передать?

Вариант 1: С помощью лямбды (анонимного метода)

int bounds = 7;
Thread t = new Thread(() => CountToNumber(bounds));
t.Start();
static void CountToNumber(int n)
{
    for (int i = 1; i <= n; i++)
    {
        Console.WriteLine($"[Поток] {i} / {n}");
        Thread.Sleep(300);
    }
}

Здесь мы оборачиваем вызов нужного нам метода в лямбду, чтобы передать параметры. Это очень распространённая практика, ведь Thread ждёт метод без параметров (ThreadStart).

Вариант 2: Используем ParameterizedThreadStart

Можно воспользоваться специальным делегатом ParameterizedThreadStart, который принимает один параметр типа object.

Thread t = new Thread(CountToNumberObject);
t.Start(12);

static void CountToNumberObject(object? n)
{
    int max = (int)n!;
    for (int i = 1; i <= max; i++)
    {
        Console.WriteLine($"[Поток] {i} / {max}");
        Thread.Sleep(200);
    }
}

Да, тип параметра — object, поэтому потребуется приведение типа. Плохо, но зато работает! Хотя современный C# предпочитает вариант с лямбдой.

4. Управление жизнью потока

Давайте разберёмся, какие полезные штуки предоставляет класс Thread.

Свойство / Метод Назначение
Thread.Start()
Запускает поток (метод, указанный при создании)
Thread.Join()
Ожидает завершения потока (блокирует вызывающий поток до конца работы другого потока)
Thread.IsAlive
Показывает, работает ли поток сейчас (true/false)
Thread.Name
Позволяет давать потоку имя (полезно для отладки)
Thread.CurrentThread
Получить объект, представляющий текущий поток
Thread.Sleep(ms)
Останавливает текущий поток на ms миллисекунд

Пример: Ждём завершения потока

Иногда нужно, чтобы главный поток дождался окончания работы вспомогательного потока.

Thread t = new Thread(() =>
{
    for (int i = 0; i < 5; i++)
    {
        Console.WriteLine($"[Второй поток] {i}");
        Thread.Sleep(300);
    }
});
t.Start();

Console.WriteLine("[Главный поток] Ждём завершения второго потока...");
t.Join(); // Главный поток ждёт здесь

Console.WriteLine("[Главный поток] Второй поток завершился!");

Без Join() программа могла бы закончиться, даже если поток работает. С Join() главный поток терпеливо подождёт, когда все дела будут завершены.

5. Полезные нюансы

Именование потоков: чтобы не запутаться

Для отладки можно давать потокам имена:

Thread t = new Thread(() =>
{
    Console.WriteLine($"Это выполняется в потоке: {Thread.CurrentThread.Name}");
});
t.Name = "Поток-Счётчик";
t.Start();

Это поможет, когда потоков станет много и каждый будет делать своё дело.

Ограничения и реальное будущее

Сразу стоит сказать: в современных приложениях ручное управление потоками через Thread — редкость. На практике часто используют более мощные и умные инструменты (Task, async/await), которые будут подробно рассмотрены чуть позже. Но понимание основ работы с потоками важно для:

  • Понимания "закулисья" C# и .NET.
  • Собеседований (иногда вас попросят объяснить разницу между Thread и Task).
  • Диагностики и устранения сложных проблем в больших, "наследуемых" приложениях.

Итоговая схема: жизненный цикл потока

stateDiagram-v2
    [*] --> New: Thread создан
    New --> Running: Start()
    Running --> Stopped: Метод завершён
    Stopped --> [*]

Теперь вы умеете самостоятельно создавать и запускать потоки в C#. Вы уже не просто пассажир в поезде, а машинист, управляющий сразу несколькими вагонами! Впереди — жизненный цикл потока, его управление, синхронизация и новые горизонты параллелизма.

6. Типичные ошибки и трюки при работе с Thread

Ошибка №1: не запускать поток.
Часто создают объект Thread, но забывают вызвать метод Start(). В результате поток не начинает работу, и причины этого сложно сразу понять.

Ошибка №2: изменение общих данных без синхронизации.
Если несколько потоков работают с одной и той же переменной без защиты, ждите проблем! Это как если два кассира раздают сдачу из одной коробки — быстро возникнет путаница и ошибки.

Ошибка №3: использование устаревших и небезопасных методов.
Не применяйте методы Thread.Suspend(), Thread.Resume() и подобные — они признаны опасными и устаревшими. Управляйте жизненным циклом потоков другими способами.

Ошибка №4: необработанные исключения внутри потоков.
Если в потоке происходит исключение, которое не поймали, поток завершится, а основной поток об этом может и не узнать! Оборачивайте код потока в блок try-catch, чтобы ловить ошибки и логировать их.

Thread t = new Thread(() =>
{
    try
    {
        // ... ваш код
    }
    catch (Exception ex)
    {
        // Логируем ошибку
        Console.WriteLine($"[Поток] Ошибка: {ex.Message}");
    }
});
t.Start();
2
Задача
C# SELF, 55 уровень, 1 лекция
Недоступна
Ожидание завершения потока (Join)
Ожидание завершения потока (Join)
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ