JavaRush /Курси /C# SELF /Життєвий цикл потоку та керування ним

Життєвий цикл потоку та керування ним

C# SELF
Рівень 55 , Лекція 2
Відкрита

1. Вступ

Уявіть, що потік — це невтомний співробітник, якому ви доручаєте роботу. Він може спати (ще не почав працювати), працювати в поті чола (виконується ваш метод), чекати, поки ви дасте йому нове завдання (простій), або завершити виконання.

У C# (та загалом у .NET) життєвий цикл потоку складається з кількох станів:

  • Unstarted — потік створено, але ще не запущено.
  • Running — потік виконується.
  • WaitSleepJoin — потік тимчасово не працює (наприклад, чекає на сигнал або «спить»).
  • Stopped — потік виконав завдання й завершився.

Цикл наочно можна подати такою схемою:

stateDiagram-v2
    [*] --> Unstarted
    Unstarted --> Running: Start()
    Running --> WaitSleepJoin: Wait/Sleep/Join
    WaitSleepJoin --> Running: Отримано сигнал/Час вийшов
    Running --> Stopped: Метод завершено
    WaitSleepJoin --> Stopped: Метод завершено
    Stopped --> [*]

Усе починається зі створення об’єкта Thread, але, доки ви не викличете Start(), потік «дрімає» у стані Unstarted. Після Start() він переходить у Running. Якщо потік усередині коду викличе Thread.Sleep або чекатиме на щось (наприклад, Monitor.Wait), він потрапить в особливий стан очікування. Щойно метод, переданий потоку, завершується, потік «помирає», перестає існувати і вже не «відродиться». Це квиток в один кінець.

2. Практика: Життєвий цикл простого потоку

Подивімося на класичний приклад:

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        // Створюємо потік — поки лише плануємо роботу
        Thread worker = new Thread(DoWork);

        Console.WriteLine($"Статус потоку після створення: {worker.ThreadState}");

        // Запускаємо потік
        worker.Start();
        Console.WriteLine($"Статус потоку після запуску: {worker.ThreadState}");

        // Дамо головному потоку трохи поспати, щоб робочий потік встиг попрацювати
        Thread.Sleep(100);

        Console.WriteLine($"Статус потоку (пізніше): {worker.ThreadState}");

        // Чекаємо, поки worker завершиться (приєднуємося)
        worker.Join();

        Console.WriteLine($"Статус потоку після завершення: {worker.ThreadState}");
        Console.WriteLine("Головний потік завершено");
    }

    static void DoWork()
    {
        Console.WriteLine("Робочий потік почав роботу!");
        Thread.Sleep(500);
        Console.WriteLine("Робочий потік завершив роботу!");
    }
}

Що виводить програма?

  1. Після створення потоку — статус буде Unstarted.
  2. Після старту — зазвичай одразу Running (але може бути й Running | Background).
  3. Під час роботи — статус може бути Running або WaitSleepJoin, якщо потік «спить».
  4. Після завершення методу — статус стає Stopped.

Цей код — чудовий інструмент, щоб зрозуміти, у якому стані може бути ваш потік. Можете погратися із затримками й подивитися, як змінюється статус.

3. Керування потоком: базові методи

Запуск: Start()

Здається очевидним, але нагадаємо: створили потік — запустіть його методом Start(). І запускати можна лише один раз: спроба викликати Start() повторно призведе до винятку ThreadStateException.

Thread t = new Thread(MyMethod);
t.Start();   // OK
t.Start();   // Помилка!

Очікування завершення: Join()

Іноді треба дочекатися, поки потік усе закінчить, і лише потім продовжити роботу. Для цього є Join().

Thread t = new Thread(MyMethod);
t.Start();
t.Join(); // Блокує поточний потік до завершення t

Якщо у вас кілька потоків, можна викликати Join() для кожного — головний потік зачекає, поки всі «працівники» не завершать.

Варіанти: є перевантаження Join(int millisecondsTimeout), яке чекає лише вказаний час, а потім продовжує роботу.

// Чекаємо не більше 2 секунд
if (t.Join(2000))
    Console.WriteLine("Потік завершився вчасно");
else
    Console.WriteLine("Чекати вже набридло...");

Примусова зупинка: чому це погана ідея

У старих версіях .NET був метод Thread.Abort(), який дозволяв «вбити» потік на льоту. Зараз його майже не зустрінеш — він небезпечний і може лишити програму в дивному стані. Філософія .NET така: потік має завершуватися добровільно. Ви ж не «вбиваєте» співробітника — ви ввічливо натякаєте, що робочий день закінчився.

4. Як коректно «зупинити» потік

Найкращий і безпечний спосіб зупинити роботу потоку — використовувати прапор скасування або ознаку завершення, яку потік періодично перевіряє.

class Worker
{
    private volatile bool shouldStop = false;

    public void DoWork()
    {
        while (!shouldStop)
        {
            Console.WriteLine("Працюю!");
            Thread.Sleep(300);
        }

        Console.WriteLine("Потік завершує роботу за командою.");
    }

    public void RequestStop()
    {
        shouldStop = true;
    }
}

Застосування:

Worker w = new Worker();
Thread t = new Thread(w.DoWork);
t.Start();

// Трішки чекаємо
Thread.Sleep(1000);

// Просимо потік завершитися
w.RequestStop();
t.Join(); // Чекаємо завершення потоку

Важливий момент: volatile

Ключове слово volatile каже компілятору й процесору: «Не кешуй це поле, завжди бери актуальне значення!» Це важливо, щоб потік побачив свіжий прапор зупинки. Без цього (або інших прийомів синхронізації) потік може нескінченно не помічати ваших змін.

5. Переходи потоків у стани очікування та сну

Іноді потік тимчасово нічого не робить — або чекає, або спить.

Сон: Thread.Sleep

Коли хочете дати потоку відпочити або пригальмувати виконання (щоб не навантажувати процесор), використовуйте Thread.Sleep(milliseconds).

// Потік спить 2 секунди
Thread.Sleep(2000);

Під час сну потік не виконує жодної роботи.

Очікування / Join

Коли головний потік чекає, поки дочірній завершиться (Join), головний стоїть «на паузі». Аналогічно, якщо потік чекає звільнення ресурсу (наприклад, за допомогою моніторів чи інших примітивів синхронізації), він переходить у спеціальний стан очікування.

6. Керування фоновістю потоку

У .NET потоки бувають двох видів: foreground (передній план) і background (фонові). Різниця проста:

  • Якщо в процесі лишилися тільки фонові потоки, процес завершиться автоматично.
  • Головний потік і всі потоки переднього плану мають завершитися, щоби процес припинив роботу.

Можна явно вказати, що потік — фоновий:

Thread t = new Thread(SomeMethod);
t.IsBackground = true; // Зробили фоновим
t.Start();

Практичний приклад — Демон vs. Звичайний потік

Thread t = new Thread(() =>
{
    while (true)
    {
        Console.WriteLine("Я фантомний (фон), мене не зупинити!");
        Thread.Sleep(500);
    }
});
t.IsBackground = true; // Робимо фоновим
t.Start();

Thread.Sleep(1200);
Console.WriteLine("Головний потік завершує роботу");
// Після завершення Main — процес «помирає», і наш вічний потік теж зникає

Після завершення Main процес завершується; фонові потоки припиняються автоматично.

7. Корисні нюанси

Чого не варто робити з потоками

  • Не можна «перезапустити» потік. Об’єкт Thread живе один раз: щойно його метод завершився — потік помер, і спроба викликати Start() знову спричинить помилку.
  • Не можна примусово зупиняти чужий потік методами Thread.Abort() або Thread.Suspend() — це застаріло й небезпечно.
  • Не можна ігнорувати завершення роботи потоку. Якщо потік працює з файлами чи ресурсами, коректно звільняйте їх перед завершенням потоку.

Перевірка стану та керування життєвим циклом

if (t.IsAlive)
{
    Console.WriteLine("Потік досі живий");
}
else
{
    Console.WriteLine("Потік завершився");
}

IsAlivetrue, доки потік виконує свій метод; після завершення — false.

Життєвий цикл простого потоку в .NET

Стан Як потрапити Що це означає? Як вийти
Unstarted
new Thread(...)
Потік створено, не запущено Викликати Start()
Running
Start()
Потік виконує роботу Завершити метод
WaitSleepJoin Sleep(), Join(), очікування Потік тимчасово неактивний Очікування закінчується
Stopped Метод потоку завершено Потік «помер» Це кінець

FAQ щодо керування життям потоку

Питання: Чи можна «вбити» потік за командою?
Відповідь: Ні й не потрібно, потоки мають самі завершувати роботу. Використовуйте прапори скасування.

Питання: Чи можна повторно використовувати об’єкт Thread?
Відповідь: Ні. Створюйте новий об’єкт для нової роботи.

Питання: Що буде, якщо головний потік завершиться, а дочірній залишиться працювати?
Відповідь: Якщо дочірній потік — фоновий (IsBackground == true), застосунок завершиться. Якщо ні — процес житиме, доки всі потоки не закінчать роботу.

Питання: Як коректно очищати ресурси, якщо потік завершує роботу через скасування?
Відповідь: Використовуйте блоки try...finally всередині методу потоку, щоб ресурси звільнялися за будь-яких умов.

8. Типові помилки й як їх уникати під час роботи з потоками

Помилка №1: повторне використання одного й того самого об’єкта Thread.
Не можна запускати один і той самий об’єкт потоку більше одного разу. Після завершення потоку його не можна перезапустити — це призведе до винятку.

Помилка №2: некоректне звільнення зовнішніх ресурсів у потоці.
Якщо потік працює з файлами, мережею чи іншими ресурсами, обов’язково забезпечте коректне закриття та звільнення. Рекомендується використовувати блоки finally або конструкції using, щоб уникнути витоків і блокувань.

Помилка №3: створення надто великої кількості потоків.
Надлишкова кількість потоків ускладнює налагодження й може призвести до зниження продуктивності. Один зайвий потік — це зайвий час на пошук і виправлення неочікуваних помилок.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ