Thread.Start существует с первой версии .NET, и большинство разработчиков знают, что он «запускает поток». Но что именно происходит в момент вызова, как передать данные в поток и почему Thread почти не используют в новом коде — стоит разобрать подробно.Как это устроено
Thread — это обёртка над потоком операционной системы. Каждый созданный объект System.Threading.Thread соответствует одному OS-потоку со своим стеком (по умолчанию 1 МБ на x64).Создать поток можно двумя способами — через
ThreadStart или ParameterizedThreadStart:// Без параметра
var t1 = new Thread(new ThreadStart(DoWork));
// С параметром типа object
var t2 = new Thread(new ParameterizedThreadStart(DoWorkWithParam));
После
new Thread(...) поток ещё не запущен. Запуск происходит при вызове Start:t1.Start(); // ThreadStart — без аргумента
t2.Start("hello"); // ParameterizedThreadStart — передаём object
Внутри метода поток получает управление не сразу: ОС ставит его в очередь планировщика. Поэтому код после
Start() в вызывающем потоке продолжает выполняться параллельно.Передача данных в поток
ParameterizedThreadStart принимает object — это неудобно, потому что нужно приводить тип внутри метода:void DoWorkWithParam(object? state)
{
var message = (string)state!;
Console.WriteLine(message);
}
Более чистый способ — захват переменной через лямбду:
string data = "hello from closure";
var t = new Thread(() => Console.WriteLine(data));
t.Start();
Тут важно понимать: лямбда захватывает ссылку на переменную, а не значение. Если
data изменится до того, как поток до неё доберётся, поток увидит новое значение.Фоновые и foreground-потоки
По умолчанию
Thread создаётся как foreground (IsBackground = false). Это значит, что процесс не завершится, пока поток не закончит работу. Если нужно обратное поведение:var t = new Thread(DoWork);
t.IsBackground = true;
t.Start();
Фоновый поток будет убит принудительно при выходе из основного потока.
Подводные камни
Thread.Abort() удалён начиная с .NET 5 — он бросал ThreadAbortException в произвольном месте и приводил к непредсказуемому состоянию программы. Для отмены нужно использовать CancellationToken.Совместный доступ к данным между потоками требует синхронизации. Без неё возникают гонки, которые воспроизводятся нестабильно и тяжело отлаживаются:
int counter = 0;
var t1 = new Thread(() => { for (int i = 0; i < 10000; i++) counter++; });
var t2 = new Thread(() => { for (int i = 0; i < 10000; i++) counter++; });
t1.Start(); t2.Start();
t1.Join(); t2.Join();
// counter будет меньше 20000 — это гонка
Console.WriteLine(counter);
Чтобы дождаться завершения потока, используют
Join. Без него нельзя гарантировать, что поток завершился до следующей строки кода.Пример кода
Корректный вариант с отменой через
CancellationToken:var cts = new CancellationTokenSource();
var t = new Thread(() =>
{
while (!cts.Token.IsCancellationRequested)
{
Console.WriteLine("Working...");
Thread.Sleep(500);
}
Console.WriteLine("Stopped.");
});
t.IsBackground = true;
t.Start();
Thread.Sleep(2000);
cts.Cancel();
t.Join();
Thread.Start запускает OS-поток, но не гарантирует немедленное выполнение. Данные лучше передавать через замыкания, а не через ParameterizedThreadStart. Для большинства задач сегодня используют Task, async/await или ThreadPool — они не блокируют поток в ожидании и экономят ресурсы. 📍 Навигация: Вакансии • Задачи • Собесы
🐸 Библиотека шарписта
#sharp_view