Від моменту створення до знищення, потік проходить через чітко визначену послідовність станів. Клас Thread надає властивість ThreadState типу System.Threading.ThreadState (flags enum), що відображає поточний стан.
Розуміння станів потоку критичне для debugging складних багатопоточних сценаріїв: коли ви бачите у debugger або дамп-файлі потік у стані WaitSleepJoin, це означає, що він заблокований на Sleep, Join або синхронізаційному примітиві — і далі слід шукати, хто або що його блокує.
ThreadState — це [Flags] enum, тому потік може одночасно мати кілька станів:
using System.Threading;
var thread = new Thread(() =>
{
Thread.Sleep(5000); // потік зависне на 5 секунд
});
// Перед Start()
Console.WriteLine($"До Start(): {thread.ThreadState}");
// Виведе: Unstarted
thread.Start();
Thread.Sleep(100); // Даємо потоку час почати виконуватись і зайти у Sleep
// Потік тепер у Sleep — стан WaitSleepJoin
Console.WriteLine($"Під час Sleep: {thread.ThreadState}");
// Виведе: WaitSleepJoin
// Або: Background, WaitSleepJoin (якщо IsBackground=true)
thread.Join();
Console.WriteLine($"Після Join: {thread.ThreadState}");
// Виведе: Stopped
// Порада: для перевірки чи потік завершився — використовуйте IsAlive
// замість ThreadState (більш читабельно)
Console.WriteLine($"IsAlive: {thread.IsAlive}"); // false після завершення
Ключові стани, що зустрічаєте на практиці:
new Thread(...)), але Start() ще не викликано. Метод не почав виконуватись.0 — за специфікою флагів, "Running" означає відсутність інших флагів.Thread.Sleep(), thread.Join(), Monitor.Wait(), lock (очікує звільнення), ManualResetEvent.WaitOne() та будь-якому іншому примітиві синхронізації.Background | Running, Background | WaitSleepJoin. Означає що IsBackground = true. Не є "справжнім" станом lifecycle, а додатковий флаг.Start() викине ThreadStateException.Thread.Abort() кидає PlatformNotSupportedException. На .NET Framework: AbortRequested — після Abort() але до обробки, Aborted — ThreadAbortException оброблено.Для перевірки чи потік ще виконується — IsAlive зручніший ніж розбір ThreadState:
bool running = thread.IsAlive;
// IsAlive == true: Running, WaitSleepJoin, SuspendRequested, Suspended, AbortRequested
// IsAlive == false: Unstarted, Stopped
Windows Thread Scheduler — preemptive (витісняючий): він може зупинити будь-який потік у будь-який момент і передати CPU іншому. Рішення приймається на основі priority class процесу і priority level потоку всередині процесу.
Справжній пріоритет (Base Priority) = Priority Class процесу + Thread Priority Level:
ThreadPriority в .NET | Windows Priority Level | Base Priority (Normal Process) |
|---|---|---|
Lowest | THREAD_PRIORITY_IDLE | 2 |
BelowNormal | THREAD_PRIORITY_BELOW_NORMAL | 6 |
Normal | THREAD_PRIORITY_NORMAL | 8 (за замовчуванням) |
AboveNormal | THREAD_PRIORITY_ABOVE_NORMAL | 10 |
Highest | THREAD_PRIORITY_HIGHEST | 12 |
Планувальник завжди вибирає готовий (Ready) потік з найвищим пріоритетом. Якщо є декілька готових потоків з однаковим пріоритетом — вони отримують кванти часу по черзі (Round Robin).
using System.Diagnostics;
using System.Threading;
int lowCount = 0, highCount = 0;
var sw = Stopwatch.StartNew();
// Потік з низьким пріоритетом
var lowPriority = new Thread(() =>
{
while (sw.ElapsedMilliseconds < 3000)
Interlocked.Increment(ref lowCount);
}) { Name = "Low", Priority = ThreadPriority.Lowest };
// Потік з високим пріоритетом
var highPriority = new Thread(() =>
{
while (sw.ElapsedMilliseconds < 3000)
Interlocked.Increment(ref highCount);
}) { Name = "High", Priority = ThreadPriority.Highest };
lowPriority.Start();
highPriority.Start();
lowPriority.Join();
highPriority.Join();
Console.WriteLine($"Low priority: {lowCount:N0} iterations");
Console.WriteLine($"High priority: {highCount:N0} iterations");
Console.WriteLine($"Ratio: {(double)highCount / lowCount:F1}x більше у High");
// На завантаженій системі: high виконає у 5-20x більше ітерацій
Priority Inversion — критична ситуація, коли низькопріоритетний потік непрямо блокує високопріоритетний. Класичний сценарій:
// Ілюстрація проблеми Priority Inversion
// (спрощена — реальна ситуація складніша)
var mutex = new object();
// Низькопріоритетний потік захоплює mutex
var lowPriority = new Thread(() =>
{
lock (mutex) // ← захопив mutex!
{
Thread.Sleep(5000); // виконує довгу роботу з mutex
}
})
{ Priority = ThreadPriority.Lowest };
// Середньопріоритетний потік "витіснив" низький (той не встигає звільнити mutex)
var mediumPriority = new Thread(() =>
{
while (true) { /* CPU-intensive робота — не відпускає планувальник */ }
})
{ Priority = ThreadPriority.Normal };
// Високопріоритетний потік чекає mutex який тримає Low, якого витісняє Medium
var highPriority = new Thread(() =>
{
lock (mutex) // ← ЧЕКАЄ! Low тримає mutex, Medium не дає Low виконатися
{
Console.WriteLine("High виконується (ніколи не відбудеться поки є Medium!)");
}
})
{ Priority = ThreadPriority.Highest };
// РЕЗУЛЬТАТ: High (найбільший пріоритет) не може виконатися
// через Medium (нижчий пріоритет!) — Priority Inversion
Starvation — низькопріоритетний потік ніколи не отримує CPU тому що завжди є потоки вищого пріоритету:
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
int lowExecutions = 0;
int highExecutions = 0;
// 4 High-priority потоки завжди готові виконуватись
for (int i = 0; i < Environment.ProcessorCount; i++)
{
new Thread(() =>
{
while (!cts.Token.IsCancellationRequested)
Interlocked.Increment(ref highExecutions);
})
{ Priority = ThreadPriority.AboveNormal }.Start();
}
// Low-priority потік — може ніколи не виконатись (starvation)
var low = new Thread(() =>
{
while (!cts.Token.IsCancellationRequested)
Interlocked.Increment(ref lowExecutions);
})
{ Priority = ThreadPriority.Lowest };
low.Start();
Thread.Sleep(10_000);
Console.WriteLine($"High: {highExecutions:N0}");
Console.WriteLine($"Low: {lowExecutions:N0}");
Console.WriteLine($"Low отримав {100.0 * lowExecutions / (highExecutions + lowExecutions):F4}% CPU часу");
// На завантаженій системі low може отримати < 0.01% CPU або взагалі нічого
Windows намагається боротися зі starvation через Dynamic Priority Boosting (автоматичне підвищення пріоритету потоків, що давно чекають), але це не гарантія. Для продакшн-систем: використовуйте Normal для всіх потоків, якщо немає надзвичайно вагомої причини.
У .NET Framework існував метод Thread.Abort(), що надсилав ThreadAbortException у цільовий потік. Це здавалось зручним: можна зупинити будь-який потік ззовні. На практиці — джерело важко відтворюваних багів:
Проблема 1: Нестійкий стан — ThreadAbortException може виникнути між будь-якими двома інструкціями IL. Якщо потік в середині операції "оновити запис у БД" — запис може залишитися в напів-оновленому стані.
Проблема 2: Lock leaks — якщо потік тримає lock і отримує Abort — він кидає виключення, lock звільняється (CLR це гарантує), але операція у критичній секції могла залишити дані некоректними.
Проблема 3: Finally blocks — ThreadAbortException перехоплюється finally і re-throw-иться автоматично. Але finally може тривати дуже довго якщо там є cleanup логіка.
Через ці проблеми Thread.Abort() у .NET Core 1.0+ повністю видалений для Windows, а у .NET 5+ — кидає PlatformNotSupportedException на всіх платформах.
Правильна архітектура — cooperative cancellation: потік сам перевіряє чи його просять зупинитись і коректно завершує роботу:
using System.Threading;
// CancellationTokenSource — об'єкт управління скасуванням
using var cts = new CancellationTokenSource();
CancellationToken token = cts.Token; // токен для передачі у потік
var worker = new Thread(() =>
{
Console.WriteLine($"[{Thread.CurrentThread.Name}] Починаємо роботу");
try
{
for (int i = 0; i < 100; i++)
{
// Варіант 1: Явна перевірка — гнучко, але вимагає ручних перевірок
if (token.IsCancellationRequested)
{
Console.WriteLine($"[{Thread.CurrentThread.Name}] Скасування виявлено на ітерації {i}");
break;
}
// Варіант 2: ThrowIfCancellationRequested — кидає OperationCanceledException
// token.ThrowIfCancellationRequested();
DoUnitOfWork(i);
// Варіант 3: Sleep з підтримкою скасування
// Thread.Sleep з CancellationToken — прокинеться при Cancel()
token.WaitHandle.WaitOne(100); // Sleep(100) з можливістю переривання
}
}
catch (OperationCanceledException)
{
Console.WriteLine($"[{Thread.CurrentThread.Name}] Коректно завершено через скасування");
}
finally
{
Console.WriteLine($"[{Thread.CurrentThread.Name}] Cleanup завершено");
// Finally ЗАВЖДИ виконається — файли закриті, з'єднання закриті
}
})
{ Name = "Worker" };
worker.Start();
// Через 2 секунди — скасовуємо
Thread.Sleep(2000);
cts.Cancel(); // встановлює IsCancellationRequested = true
worker.Join(); // чекаємо коректного завершення
Console.WriteLine("Потік завершився коректно");
void DoUnitOfWork(int iteration)
{
// Якась осмислена робота
Thread.Sleep(50);
}
using System.Threading;
// Ручне скасування
using var cts1 = new CancellationTokenSource();
cts1.Cancel(); // негайне скасування
// або
cts1.CancelAfter(TimeSpan.FromSeconds(5)); // відкладене скасування через 5s
cts1.CancelAfter(5000); // або у мілісекундах
// Скасування за таймаутом від початку
using var cts2 = new CancellationTokenSource(TimeSpan.FromSeconds(30));
// Ланцюгові токени: скасовується якщо будь-який з джерел скасовано
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(
cts1.Token, cts2.Token
);
var token = linkedCts.Token;
// Підписка на скасування (викликається у ThreadPool, не у головному потоці)
token.Register(() =>
{
Console.WriteLine("Скасування виявлено через Register callback!");
});
// Перевірка
Console.WriteLine($"IsCancellationRequested: {token.IsCancellationRequested}");
// ThrowIfCancellationRequested — стандартна перевірка у циклах
token.ThrowIfCancellationRequested(); // кидає OperationCanceledException
Thread.Interrupt() — більш м'яка альтернатива Abort для конкретної ситуації: пробудити потік, що заблокований у WaitSleepJoin стані:
var thread = new Thread(() =>
{
try
{
Console.WriteLine("Потік: чекаємо 60 секунд...");
Thread.Sleep(60_000); // або Monitor.Wait(), Join(), etc.
Console.WriteLine("Потік: прокинулись нормально");
}
catch (ThreadInterruptedException)
{
// Кидається коли Interrupt() перериває WaitSleepJoin
Console.WriteLine("Потік: перерваний через Interrupt()");
// Можна зробити cleanup і завершити
}
});
thread.Start();
Thread.Sleep(2000);
thread.Interrupt(); // перериває Thread.Sleep → кидає ThreadInterruptedException
thread.Join();
Thread.Interrupt() безпечніший за Abort() оскільки не може перервати довільну операцію — лише стан очікування. Якщо потік не у WaitSleepJoin — прапорець зберігається, і ThreadInterruptedException буде кинуто при наступному вході у стан очікування. Проте для нового коду рекомендується CancellationToken.Іноді потрібна змінна, що має окреме значення для кожного потоку — без синхронізації. Класичний приклад: лічильник статистики, Random-генератор (не thread-safe).
using System.Threading;
// ThreadLocal<T>: кожен потік отримує власну копію
// valueFactory: виклик для КОЖНОГО нового потоку окремо
using var localRandom = new ThreadLocal<Random>(
valueFactory: () => new Random(Thread.CurrentThread.ManagedThreadId),
trackAllValues: true // дозволяє читати .Values (всі значення)
);
var threads = Enumerable.Range(0, 5).Select(i =>
{
return new Thread(() =>
{
// localRandom.Value — ЦЯ копія для поточного потоку
// Ніякої синхронізації не потрібно!
int rng = localRandom.Value!.Next(1, 100);
Console.WriteLine($"[Thread {Thread.CurrentThread.ManagedThreadId}] Random: {rng}");
Thread.Sleep(rng);
}) { Name = $"RngThread-{i}" };
}).ToList();
foreach (var t in threads) t.Start();
foreach (var t in threads) t.Join();
// Доступ до всіх значень (лише якщо trackAllValues: true)
// Console.WriteLine($"Ініціалізованих значень: {localRandom.Values.Count}");
Порівняння зі static полем:
static T field | [ThreadStatic] static T | ThreadLocal<T> | |
|---|---|---|---|
| Ініціалізація | Одна для процесу | Тільки для першого потоку! | Factory для кожного |
| Безпека | Thread-unsafe | Кожен потік — окрема копія | Кожен потік — окрема копія |
Dispose | Ні | Ні | Так, реалізує IDisposable |
trackAllValues | Ні | Ні | Так, якщо потрібно |
Сьогоднішній наскрізний приклад: паралельний пошук простих чисел у діапазонах із підтримкою запиту на зупинку через CancellationToken.
using System;
using System.Diagnostics;
using System.Threading;
using System.Collections.Generic;
record RangeResult(
string WorkerName,
long RangeStart,
long RangeEnd,
List<long> Primes,
TimeSpan Duration,
bool WasCancelled
);
static RangeResult FindPrimesInRange(
long start,
long end,
CancellationToken cancellationToken)
{
string workerName = Thread.CurrentThread.Name
?? $"Worker-{Thread.CurrentThread.ManagedThreadId}";
var primes = new List<long>();
var sw = Stopwatch.StartNew();
bool cancelled = false;
Console.WriteLine($"[{workerName}] Починаємо діапазон [{start:N0}..{end:N0}]");
for (long n = start; n <= end; n++)
{
// Перевірка скасування кожні 10000 чисел (не на кожній ітерації — overhead)
if (n % 10_000 == 0)
{
if (cancellationToken.IsCancellationRequested)
{
cancelled = true;
Console.WriteLine($"[{workerName}] Скасовано на {n:N0}. Зібрали {primes.Count} простих.");
break;
}
}
if (IsPrime(n))
primes.Add(n);
}
sw.Stop();
if (!cancelled)
Console.WriteLine($"[{workerName}] Завершили [{start:N0}..{end:N0}] → {primes.Count} простих за {sw.Elapsed.TotalSeconds:F2}s");
return new RangeResult(workerName, start, end, primes, sw.Elapsed, cancelled);
}
static bool IsPrime(long n)
{
if (n < 2) return false;
if (n == 2) return true;
if (n % 2 == 0) return false;
for (long d = 3; d * d <= n; d += 2)
if (n % d == 0) return false;
return true;
}
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;
static class Program
{
static void Main(string[] args)
{
Console.OutputEncoding = System.Text.Encoding.UTF8;
const long MaxNumber = 10_000_000; // шукаємо прості до 10 млн
int threadCount = Environment.ProcessorCount; // потоки = кількість ядер
Console.WriteLine($"=== Parallel Prime Finder ===");
Console.WriteLine($"Діапазон: [2 .. {MaxNumber:N0}]");
Console.WriteLine($"Потоків: {threadCount}");
Console.WriteLine("Ctrl+C для дострокової зупинки...\n");
using var cts = new CancellationTokenSource();
// Обробник Ctrl+C — коректне скасування
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true; // не завершувати процес одразу
Console.WriteLine("\n[Main] Отримано Ctrl+C — ініціюємо скасування...");
cts.Cancel();
};
// Розподіляємо діапазон між потоками
long chunkSize = (MaxNumber - 2) / threadCount + 1;
var threads = new Thread[threadCount];
var results = new RangeResult[threadCount];
for (int i = 0; i < threadCount; i++)
{
int idx = i; // ⚠️ closure pitfall fix
long start = 2 + idx * chunkSize;
long end = Math.Min(start + chunkSize - 1, MaxNumber);
threads[idx] = new Thread(() =>
{
results[idx] = FindPrimesInRange(start, end, cts.Token);
})
{
Name = $"PrimeFinder-{idx + 1}",
IsBackground = true,
Priority = ThreadPriority.BelowNormal // не заважаємо системі
};
}
var totalSw = Stopwatch.StartNew();
// Запускаємо всі потоки
foreach (var t in threads)
t.Start();
// Чекаємо завершення всіх (або скасування)
foreach (var t in threads)
t.Join(); // Join не перервати через Cancel — Join з таймаутом
totalSw.Stop();
PrintResults(results, totalSw.Elapsed, cts.Token.IsCancellationRequested);
}
static void PrintResults(RangeResult[] results, TimeSpan totalTime, bool cancelled)
{
Console.WriteLine("\n" + new string('═', 70));
Console.WriteLine(cancelled ? "⚠️ РЕЗУЛЬТАТИ (СКАСОВАНО ДОСТРОКОВО)" : "📊 ПІДСУМОК");
Console.WriteLine(new string('─', 70));
long totalPrimes = 0;
foreach (var r in results.Where(x => x is not null))
{
string status = r.WasCancelled ? " [SCR]" : "";
Console.WriteLine(
$"{r.WorkerName,-18} [{r.RangeStart,10:N0} .. {r.RangeEnd,10:N0}]" +
$" {r.Primes.Count,6} простих {r.Duration.TotalSeconds,5:F2}s{status}");
totalPrimes += r.Primes.Count;
}
Console.WriteLine(new string('─', 70));
Console.WriteLine($"Разом знайдено: {totalPrimes:N0} простих чисел");
Console.WriteLine($"Загальний час: {totalTime.TotalSeconds:F2}s");
if (totalPrimes > 0)
{
var allPrimes = results.Where(x => x is not null)
.SelectMany(r => r.Primes)
.OrderByDescending(p => p)
.Take(5);
Console.Write("Топ-5 найбільших: ");
Console.WriteLine(string.Join(", ", allPrimes));
}
Console.WriteLine(new string('═', 70));
}
}
dotnet new console -n PrimeFinder
# Скопіювати код у Program.cs
dotnet run
# Для дострокової зупинки: Ctrl+C
Thread Lifecycle та ThreadState
IsAlive — зручніше ніж розбір ThreadStateПріоритети
NormalБезпечне Завершення
Thread.Abort() → PlatformNotSupportedException у .NET 5+CancellationToken — cooperative cancellationIsCancellationRequested у циклахtoken.WaitHandle.WaitOne(ms) — Sleep, що перериваєтьсяThreadLocal\<T\>
Напишіть програму що запускає 3 потоки і моніторить їх ThreadState кожні 500ms. Потоки мають різні сценарії:
ManualResetEventSlim що встановлюється через 5 секундВиводьте таблицю ThreadName | ThreadState | IsAlive кожні 500ms.
Реалізуйте клас BackgroundWorker<T>:
(Func<CancellationToken, T> work)Start() — запускає фоновий потікCancel() — надсилає скасуванняT GetResult(int timeoutMs) — чекає результату або кидає TimeoutExceptionbool IsCancelled — чи було скасованоРеалізуйте BoundedThreadPool:
minThreads, maxThreadsBlockingCollection<Action> або власна чергаmaxThreadsQueueWork(Func<CancellationToken, Task> work) — додати задачуShutdownAsync() — дочекатись завершення всіх задач, зупинити потокиПотоки — Основи та API Thread
Глибокий академічний розбір класу Thread — делегати, closure питфол, результати з потоків, foreground vs background та ключові методи. Теорія і практика від першого потоку до паралельного завантажувача.
Проблеми Спільного Стану — Race Condition та Data Race
Глибокий академічний розбір проблем спільного стану в багатопоточних програмах — Race Condition, Data Race, Torn Reads та Visibility Problem. Теорія, аналогії, демонстрації з кодом і поясненням на рівні процесора.