Класс Thread в C# — создание, Start, фоновые потоки и практика
Обзорная глава про System.Threading.Thread в .NET: что происходит при Start, как передать данные, чем foreground- и background-потоки отличаются для процесса, и как это соотносится с Task, пулом потоков и async/await. Базовый контекст по асинхронности и синхронизации — в статье Асинхронность и многопоточность.
Что такое Thread в модели .NET
Thread — управляемая обёртка над потоком операционной системы (kernel thread): у потока есть собственный стек вызовов, свой указатель инструкций и состояние планировщика ОС. CLR создаёт и учитывает этот поток, но планирование (когда именно поток получит процессорное время) выполняет ОС.
Свойства вроде Thread.ManagedThreadId удобны для логов; Thread.CurrentThread в любом месте кода даёт объект потока, который сейчас выполняет этот участок.
Создание экземпляра и запуск
Делегаты ThreadStart и ParameterizedThreadStart
Поток создают, передав в конструктор Thread метод, который будет точкой входа:
ThreadStart— сигнатураvoid();ParameterizedThreadStart— сигнатураvoid(object? state); приStart(object?)CLR передаёт этот аргумент в делегат.
void Worker() => Console.WriteLine("ok");
void WorkerWithState(object? state)
{
var text = (string)state!;
Console.WriteLine(text);
}
var a = new Thread(Worker);
var b = new Thread(WorkerWithState);
a.Start();
b.Start("hello");
После new Thread(...) поток ОС ещё не обязан существовать в полном смысле рабочей нити: фактический запуск инициируется вызовом Start() (или Start(parameter) для ParameterizedThreadStart).
Что делает Start
Start переводит поток из состояния «создан / ещё не запущен» в состояние «допущен к выполнению»: CLR взаимодействует с ОС, поток попадает в очередь готовых к выполнению. Код после Start() в вызывающем потоке продолжается сразу; тело рабочего метода может начаться через произвольную задержку (микросекунды, миллисекунды) в зависимости от загрузки CPU и политики планировщика, поэтому к следующей строке в инициаторе рабочий метод может ещё не стартовать. Чтобы дождаться завершения фоновой работы перед продолжением, используют Join (см. ниже).
Однократный запуск
У одного объекта Thread Start() вызывают один раз. Повторный вызов приводит к InvalidOperationException. «Перезапустить» тот же экземпляр нельзя — нужен новый Thread.
Передача данных в поток
ParameterizedThreadStart и приведение типов
Аргумент Start(object?) имеет тип object. Внутри метода обычно делают приведение к ожидаемому типу. Ошибка типа проявится уже во время выполнения (InvalidCastException), компилятор это не проверит.
Замыкание (лямбда)
Частый приём — передать лямбду, которая захватывает локальные переменные или поля:
var message = "hello";
var t = new Thread(() => Console.WriteLine(message));
t.Start();
Захват — по ссылке на переменную (для ссылочных типов и полей структур в замыкании), а не «снимок значения на момент Start». Если до чтения в потоке переменную изменят, поток увидит актуальное значение.
Классический подводный камень — цикл for с захватом счётчика без локальной копии:
for (int i = 0; i < 3; i++)
{
new Thread(() => Console.WriteLine(i)).Start(); // может печатать только 3
}
Надёжный приём — ввести локальную копию на итерацию: int j = i; и использовать j внутри лямбды.
Потокобезопасная передача «снимка»
Если нужно передать неизменяемый снимок данных, передают копию (например, record, string, новый объект с зафиксированными полями**) или структуру по значению в замыкании так, чтобы рабочий поток не делился изменяемым состоянием без синхронизации. Общие правила — в разделе про синхронизацию в главе про асинхронность.
Стек, имя, приоритет
Размер стека
У конструктора Thread есть перегрузки с maxStackSize (размер стека в байтах). На практике это нужно редко (глубокая рекурсия, специфичные сценарии). Значение по умолчанию зависит от платформы и режима процесса (типичный порядок — около 1 МБ на поток на настольных 64-bit; точные цифры зависят от ОС и настроек).
Name
Thread.Name полезен в отладчике и в логах. Установить имя лучше до Start, хотя технически присвоение возможно и позже в отдельных конфигурациях; единообразно — до запуска.
Priority
Thread.Priority меняет относительный приоритет потока в ОС. В прикладном коде его часто оставляют по умолчанию: завышение приоритетов множеством потоков ухудшает отзывчивость системы и редко даёт устойчивый выигрыш. Профилирование и архитектура (очереди работ, пул) обычно важнее.
Foreground и background (IsBackground)
По умолчанию IsBackground == false: поток считается foreground. Пока жив хотя бы один foreground-поток, CLR удерживает процесс живым (упрощённо: «процесс не завершится, пока эти потоки не закончились»).
Если IsBackground = true, поток не удерживает процесс: при завершении всех foreground-потоков среда может завершить процесс, а фоновые потоки получат жёсткое обрывание без гарантий «дойти до конца метода». Поэтому фоновые потоки подходят для вспомогательной работы, но критичные сбросы буферов и транзакции должны завершаться в foreground-потоке или через явные точки синхронизации (Join, CancellationToken, shutdown hooks приложения).
Ожидание и пауза
Join
Join() блокирует вызывающий поток до завершения целевого потока (или до таймаута в перегрузке с TimeSpan / миллисекундами). Это простой способ упорядочить наблюдаемое поведение после Start.
Thread.Sleep
Thread.Sleep блокирует текущий поток на указанное время. В библиотечном и серверном коде для ожиданий и таймаутов предпочтительнее await Task.Delay(..., cancellationToken), чтобы не занимать поток пула. Sleep(0) и Thread.Yield() — низкоуровневые подсказки планировщику; в прикладной логике почти не нужны.
Interrupt
Thread.Interrupt() на заблокированном потоке (Wait, Sleep, некоторые I/O) может вызвать ThreadInterruptedException. Это кооперативный сигнал, а не гарантированная «отмена любой работы». В новом коде для отмены доминирует CancellationToken.
Модель квартир (STA / MTA)
Свойство Thread.SetApartmentState / GetApartmentState относится к COM и к старым UI-фреймворкам (WinForms, WPF): UI-поток обычно STA. Для чистого вычислительного кода на современном .NET без COM это редко трогают. Подробности — в документации по interop и UI; ошибка выбора модели проявляется как проблемы маршалинга или «чужой поток обновляет UI».
Отмена и завершение
Thread.Abort удалён
Thread.Abort в современном .NET (начиная с .NET 5 / .NET Core) недоступен. Раньше он выбрасывал ThreadAbortException в произвольной точке стека и ломал инварианты lock, using и библиотечного состояния. Замена — кооперативная отмена через CancellationToken, флаги завершения и корректный выход из циклов.
Пример с отменой
using var cts = new CancellationTokenSource();
var worker = new Thread(() =>
{
while (!cts.Token.IsCancellationRequested)
{
Console.WriteLine("work");
Thread.Sleep(500);
}
Console.WriteLine("exit");
});
worker.IsBackground = true;
worker.Start();
Thread.Sleep(2000);
cts.Cancel();
worker.Join();
Исключения в рабочем потоке
Необработанное исключение в потоке, созданном через Thread, ведёт себя в зависимости от версии и политики приложения: в консоли это часто аварийное завершение процесса после событий вроде AppDomain.UnhandledException (в .NET Framework картина была богаче по настройкам). Практический вывод: оборачивайте тело рабочего метода в try/catch, логируйте, пробрасывайте в общий канал ошибок или завершайте приложение осознанно.
Синхронизация и общие данные
Одновременная запись в одно поле из нескольких потоков без примитивов даёт гонки и «рваные» чтения. Минимальный учебный пример — инкремент счётчика в цикле из двух потоков: результат часто меньше суммы итераций.
Примитивы (lock, Interlocked, Monitor, SemaphoreSlim, коллекции из System.Collections.Concurrent) — в главе про асинхронность и параллелизм. volatile и барьеры памяти — отдельная тема; volatile сам по себе не заменяет взаимное исключение для составных инвариантов.
Когда выбирают Thread, а когда Task и пул
| Ситуация | Типичный выбор |
|---|---|
| Долгоживущий выделенный поток с блокирующим циклом (редко) | new Thread(...) |
| Много коротких работ, сервер, UI без блокировок | Task, async/await, ThreadPool |
| CPU-работа из синхронного кода | Task.Run (пул), а не отдельный Thread на каждую мелочь |
| Параллельный обход с разбиением по ядрам | Parallel.For / PLINQ (всё равно опираются на пул) |
Task.Run ставит работу в очередь пула потоков и переиспользует потоки; new Thread каждый раз просит ОС о отдельном потоке со своим стеком — это дороже при большом числе потоков. Зато Thread даёт предсказуемую модель «ровно один поток на объект», что иногда нужно для совместимости с нативным кодом, отдельной культуры, долгого блокирующего цикла или учебных демонстраций.
async/await освобождает поток на время I/O и хорошо стыкуется с ASP.NET Core и современными API (HttpClient, File.ReadAllTextAsync, EF Core async). Это другой слой абстракции: он не отменяет необходимость думать о гонках, если фоновые задачи трогают общие mutable-данные.
Краткая памятка
Startставит поток в очередь готовых; порядок и момент первого выполнения недетерминированы относительно кода послеStart.Joinсинхронизирует наблюдение «поток закончился».IsBackgroundопределяет, удерживает ли поток процесс живым.- Данные удобно передавать лямбдой; следите за захватом переменных цикла.
- Отмена — через
CancellationTokenи кооперативные циклы;Thread.Abortв современном .NET отсутствует. - Для большинства приложений пул +
Task+async/await— основной инструмент;Threadостаётся осознанным низкоуровневым выбором.
Официальная справка: Managed threading, Thread class.
Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.
См. также
Другие статьи этого же раздела в боковом меню (как на странице "О разделе"). C# как язык платформы .NET - устройство проекта, роль `.cs`-файлов и базовые принципы организации кода. C# — это современный, типизированный язык программирования общего назначения, разработанный корпорацией Microsoft. Справочник-шпаргалка по конфигурациям в C — типы, синтаксис, стандартная библиотека, типовые паттерны. Не заменяет пошаговое обучение. Учебный курс — раздел. Набор советов, правил, принципов и обычаев в разработке на этом языке. Кавычки, точки, запятые, скобки и прочие знаки препинания. Ключевые слова C# - назначение базовых конструкций языка и примеры их применения в типичном коде. Набор функций, которые включены в стандартную библиотеку языка. Пространства имён в C# - организация модулей, `file-scoped namespace` и поддержание чистой структуры кода. манипулировать данными (арифметические, логические, сравнительные операторы). Самый базовый способ ветвления — оператор if. Он проверяет условие и, если оно истинно (true), выполняет блок кода. Обработка исключений в C# - типы исключений, `try/catch/finally` и практики надежного кода. Платформо-зависимые исключения — например, PlatformNotSupportedException используется в кроссплатформенных API, когда функция недоступна на текущей ОС.C# - язык программирования платформы .NET
Что требуется знать перед началом изучения языка программирования C#
Справочник по конфигурациям в C#
Рекомендации по разработке на C#
Синтаксис и пунктуация в C#
Ключевые слова языка C#
Встроенные функции и методы C#
Пространства имён в C#
Управляющие конструкции и логические операторы
Условные выражения и ветвления
Обработка исключений в C#
Иерархия классов исключений в C#