1. Вступ
Якщо уявити процес як супермаркет, то потоки (thread) — це касири на різних касах, які одночасно обслуговують покупців. Усі касири працюють в одному магазині, але кожен із них виконує своє завдання паралельно, завдяки чому робота відбувається швидше та ефективніше. Потоки всередині процесу дають змогу виконувати кілька завдань одночасно, поділяючи спільні ресурси й координуючи роботу в межах одного застосунку. Головна перевага — потоки справді можуть працювати паралельно, якщо процесор підтримує багатозадачність.
Практична необхідність потоків
Навіщо взагалі запускати кілька потоків? Ось лише кілька ситуацій із реального життя:
- Ви створюєте застосунок із графічним інтерфейсом і не хочете, щоб він «зависав» під час тривалої операції.
- Потрібно одночасно завантажувати кілька файлів.
- У грі супротивники мають мислити самостійно, незалежно один від одного.
Й досі в багатьох програмах трапляються «зависання» через невміле керування потоками. Сьогодні ви навчитеся уникати таких непорозумінь.
Клас Thread: база ручної багатопоточності
Клас Thread — динозавр серед багатопоточного програмування в .NET. Попри появу сучасніших інструментів (Task, async/await), робота з Thread і далі має сенс, особливо якщо хочеться відчути себе володарем потоків «з чистого аркуша».
Схема створення потоку
- Створити об’єкт класу Thread, передавши йому метод, який виконуватиметься в потоці.
- Запустити потік за допомогою методу Start().
- (Необов’язково) За бажання можна подивитися, що відбувається — адже все може виконуватися паралельно!
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.
| Властивість / Метод | Призначення |
|---|---|
|
Запускає потік (метод, переданий під час створення) |
|
Очікує завершення потоку (блокує потік, що викликав, доки інший не завершиться) |
|
Показує, чи працює потік зараз (true/false) |
|
Дає змогу задати імʼя потоку (корисно для налагодження) |
|
Повертає об’єкт, який представляє поточний потік |
|
Призупиняє поточний потік на вказаний проміжок у мілісекундах |
Приклад: чекаємо завершення потоку
Іноді потрібно, щоб головний потік дочекався завершення роботи допоміжного потоку.
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();
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ