JavaRush /Курси /C# SELF /Пріоритети потоків і типи потоків

Пріоритети потоків і типи потоків

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

1. Вступ

Уявімо, що потоки — це люди в черзі по морозиво. Хтось стоїть спокійно, а хтось підіймає шум («Мені скоро на поїзд, пропустіть!»), і продавець наче чує всіх, але інколи вирішує обслугувати когось поза чергою — наприклад, маму з дитиною, яка плаче. Пріоритети потоків — це приблизно та сама історія.

У C# (точніше, у .NET) кожен потік має свій пріоритет — натяк для операційної системи, наскільки цей потік «важливий» порівняно з іншими. Це не суворий закон для ОС (ніхто не гарантує, що найпріоритетніший потік завжди отримає усю увагу), але зазвичай потоки з вищим пріоритетом отримують більше процесорного часу.

Де це реально потрібно?

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

2. Пріоритети потоків: як це влаштовано

У C# працювати з пріоритетом потоку просто — в об’єкта Thread є властивість Priority. Вона приймає значення з переліку ThreadPriority:

Значення Опис
Lowest
Найнижчий пріоритет
BelowNormal
Нижче середнього
Normal
Звичайний (за замовчуванням)
AboveNormal
Вище середнього
Highest
Найвищий пріоритет

Приклад коду:

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Thread lowPriorityThread = new Thread(PrintLowPriority);
        Thread highPriorityThread = new Thread(PrintHighPriority);

        lowPriorityThread.Priority = ThreadPriority.Lowest;
        highPriorityThread.Priority = ThreadPriority.Highest;

        lowPriorityThread.Start();
        highPriorityThread.Start();
    }

    static void PrintLowPriority()
    {
        for (int i = 0; i < 10; i++)
        {
            Console.WriteLine("Низький пріоритет: " + i);
            Thread.Sleep(10); // Додамо паузу, щоб побачити різницю
        }
    }

    static void PrintHighPriority()
    {
        for (int i = 0; i < 10; i++)
        {
            Console.WriteLine("Високий пріоритет: " + i);
            Thread.Sleep(10);
        }
    }
}

На замітку

На практиці, особливо на сучасних багатоядерних системах і в керованому середовищі .NET, пріоритет — це лише рекомендація операційній системі. ОС (і .NET) спробує виділити більше часу потоку з високим пріоритетом, але жодних залізних гарантій немає. Наприклад, якщо ваш потік забиратиме весь процесорний час із пріоритетом Highest, інтерфейс може почати пригальмовувати, а інші потоки потерпатимуть.

3. Зміна пріоритетів потоків

Потоки і пріоритети

graph LR
    A[Потік 1 - Lowest] -->|Менше процесорного часу| OS(Операційна система)
    B[Потік 2 - Normal] --> OS
    C[Потік 3 - Highest] -->|Більше процесорного часу| OS
    OS --> CPU(Процесор)

Навіщо змінювати пріоритет потоку?

Може здатися, що завжди можна ставити Highest, але це погана ідея! Уявіть, якби всі покупці в магазині одночасно почали вимагати: «Обслугуйте мене першим!».

Змінюйте пріоритет лише тоді, коли це обґрунтовано:

  • Фоновий потік запису логів — можна поставити BelowNormal або Lowest.
  • Потік, що обробляє користувацьке введення, — AboveNormal.
  • Процесорномісткі завдання, які не критичні за часом, — низький пріоритет.

Лайфхак: Якщо застосунок починає пригальмовувати, перевірте, чи немає у вас «нахабних» потоків із високим пріоритетом, які заважають іншим.

4. Типи (категорії) потоків у .NET

У C# традиційно виділяють два типи потоків: foreground і background. Назва background може збивати з пантелику: такий потік не блокує завершення процесу. Розберімося, у чому різниця.

Foreground-потоки (основні)

  • За замовчуванням усі створювані вами потоки — foreground.
  • Поки в процесі живий хоча б один foreground-потік, застосунок не завершиться, навіть якщо все інше вже «померло».
  • Приклад: основний потік програми (Main), а також усі потоки, створені через new Thread() без додаткових змін.

Background-потоки (фонові)

  • Якщо всі foreground-потоки завершилися й лишилися лише background-потоки, процес просто завершується, а такі потоки перериваються без попередження.
  • Використовуються для другорядних завдань: логування, фонова відправка метрик тощо.
  • Щоб зробити потік фоновим, встановіть властивість IsBackground у значення true:
Thread t = new Thread(SomeMethod);
t.IsBackground = true;
t.Start();

Демонстрація різниці

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        Thread backgroundThread = new Thread(() =>
        {
            for (int i = 0; i < 10; i++)
            {
                Console.WriteLine("Фоновий потік працює... " + i);
                Thread.Sleep(500);
            }
            Console.WriteLine("Фоновий потік ЗАВЕРШИВСЯ.");
        });

        backgroundThread.IsBackground = true; // Робимо потік фоновим!

        backgroundThread.Start();

        Console.WriteLine("Main завершує роботу за 1 секунду.");
        Thread.Sleep(1000); // Чекаємо секунду
        Console.WriteLine("Main завершився. Що буде з фоновим потоком?");
    }
}

Що побачите?
Main може завершитися, а background-потік буде перервано на півдорозі. Це зручно для завдань, які не повинні заважати закриттю застосунку.

5. Типи потоків

ThreadPool (пул потоків)

  • ThreadPool — це механізм, який керує пулом потоків, щоб не створювати їх забагато.
  • Його використовують, коли завдань багато й вони короткі: паралельна обробка запитів, асинхронні операції.
  • Потоки з пулу завжди є background — вони не заважатимуть завершенню застосунку.

Приклад: запуск коду в пулі потоків

using System;
using System.Threading;

class Program
{
    static void Main()
    {
        ThreadPool.QueueUserWorkItem(DoWorkInThreadPool, "фонове завдання");
        Console.WriteLine("Main завершує роботу.");
        Thread.Sleep(500);
    }

    static void DoWorkInThreadPool(object? state)
    {
        Console.WriteLine("Потік з пулу: " + state);
        Thread.Sleep(1000); // Зробимо паузу
        Console.WriteLine("Потік з пулу завершився!");
    }
}

На що звернути увагу:
Якби Main завершився раніше, потік із пулу могли б обірвати. Якщо потрібна гарантія завершення — використовуйте foreground-потоки або явно дочекайтеся завершення.

Еволюція багатопоточності: завдання, async/await

У сучасних застосунках рідко використовують ручне створення потоків (Thread). Зазвичай застосовують Task та асинхронні методи. Важливо знати:

  • Потоки з пулу і завдання Task працюють у background-потоках.
  • Для більшості завдань не потрібно змінювати пріоритет — дотримуйтеся здорового глузду й кращих практик.

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

Властивості та поведінка потоків

Властивість Foreground‑потік Background‑потік Потік із ThreadPool
IsBackground
false за замовчуванням true true
Завершення застосунку Застосунок не завершується, доки живий хоча б один foreground‑потік Застосунок завершиться, якщо всі foreground‑потоки завершилися Застосунок завершиться одразу після завершення Main
Керування Повний контроль Повний контроль Немає прямого контролю
Де використовується Довгі, важливі завдання (наприклад, сервер БД) Некритичні завдання, фонові операції, логування Короткі завдання, Task, асинхронні операції
Пріоритет Можна встановлювати Можна встановлювати Не рекомендується змінювати

Особливості та зауваження

  • Змінювати пріоритет можна лише для звичайних потоків (Thread), а не для завдань із пулу (Task, ThreadPool).
  • Потоки з пулу завжди мають пріоритет Normal (змінювати не можна).
  • Task і async/await — більш сучасний підхід, за якого питання пріоритетів і фонового виконання заховані «за лаштунками».

7. Типові помилки з пріоритетами і типами потоків

Помилка № 1: зловживання пріоритетами.
Призначення всім потокам Highest або всім Lowest без об’єктивної причини не приносить користі. Це може порушити баланс виконання завдань і знизити відгукливість застосунку.

Помилка № 2: неявне завершення background-потоків.
Якщо важлива робота (наприклад, збереження даних) виконується у background‑потоці й ви не чекаєте її завершення, є ризик втратити дані. Background‑потоки автоматично перериваються під час завершення процесу.

Помилка № 3: завищені очікування від пріоритету потоку.
Пріоритет потоку (Priority) — це лише рекомендація операційній системі, а не гарантія, що потік буде виконаний першим.

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