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)
Призупиняє поточний потік на вказаний проміжок у мілісекундах

Приклад: чекаємо завершення потоку

Іноді потрібно, щоб головний потік дочекався завершення роботи допоміжного потоку.

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();
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ