Как стать автором
Обновить
115.77
beeline cloud
Безопасный облачный провайдер

Многопоточность. Снизу вверх. Потоки в языке C#

Уровень сложностиСредний
Время на прочтение6 мин
Количество просмотров3.3K

Привет, Хабр! Это Дмитрий Бахтенков. Добро пожаловать в третью часть цикла статей «Многопоточность. Снизу вверх»! Мы уже разобрали процессор и операционную систему, а сегодня поговорим про использование потоков в .NET с помощью языка программирования C#.

Эта статья — обзор основных возможностей взаимодействия с потоками в .NET.

Многопоточность. Снизу вверх. Потоки в языке C#
Многопоточность. Снизу вверх. Потоки в языке C#

Класс Thread

Класс Thread — это базовый инструмент для создания и управления потоками в C#. Он позволяет запускать отдельные блоки кода параллельно на разных ядрах, что помогает эффективнее использовать ресурсы процессора и повышает отзывчивость приложений.

Создание потока

В первую очередь нам необходимо создать экземпляр класса Thread и передать в его конструктор какой-либо метод или делегат. Например:


var thread = new Thread(() =>  
{  
Console.WriteLine($"Hello World from thread {Thread.CurrentThread.ManagedThreadId}!");  
});   
thread.Start();

Выводом этой программы будет что-то вроде:

Hello World from thread 8!

В этом примере кода мы использовали статическое свойство Thread.CurrentThread для получения информации о потоке. В данном случае — для получения идентификатора созданного потока.

Когда операционная система создает поток, ему выделяется некоторый идентификатор. У основного потока (Main thread), в рамках которого работает метод Program.Main, этот идентификатор равен единице. Дополним наш пример:


static void Main()  
{  
Console.WriteLine($"Hello World from thread {Thread.CurrentThread.ManagedThreadId}!");  
var thread = new Thread(() =>  
{  
     Console.WriteLine($"Hello World from thread {Thread.CurrentThread.ManagedThreadId}!");  
});  
  thread.Start();  
}

Вывод:

Hello World from thread 1!
Hello World from thread 10!

Ожидание потока

Часто бывает так, что на каком-то из этапов программы нам необходимо дождаться окончания выполнения потока. Для этого используется метод Thread.Join. А для имитации долгой работы — статический метод Thread.Sleep(), который «усыпляет» поток: останавливает его выполнение на переданное количество миллисекунд.


static void Main()  
{  
Thread worker = new Thread(() =>  
{  
     Console.WriteLine("Рабочий поток начал выполнение.");  
     Thread.Sleep(1000); // Имитируем выполнение работы  
     Console.WriteLine("Рабочий поток завершил выполнение.");  
});  
worker.Start();  
worker.Join();  
Console.WriteLine("Основной поток продолжает выполнение после завершения рабочего потока.");  
}

Вывод программы:

Рабочий поток начал выполнение.
Рабочий поток завершил выполнение.

Основной поток продолжает выполнение после завершения рабочего потока.

Что будет, если убрать вызов метода Join из этой программы? Вот что:

Основной поток продолжает выполнение после завершения рабочего потока.
Рабочий поток начал выполнение.
Рабочий поток завершил выполнение.

Статусная модель потока

В .NET каждый поток имеет набор состояний, которые можно отследить с помощью свойства Thread.ThreadState. Оно возвращает комбинацию флагов, описывающих текущее состояние потока. Важно понимать, что эти состояния могут комбинироваться, а их значение динамически меняется по мере выполнения потока. Рассмотрим основные состояния:

  • Unstarted
    Поток находится в этом состоянии до вызова метода Start(). Он еще не начал выполнение и не задействует системные ресурсы для выполнения кода.

  • Running
    После вызова Start() поток переходит в состояние Running, что означает, что он активно выполняется. Однако это состояние может быстро изменяться, поскольку поток может попасть в ожидание или блокировку.

  • WaitSleepJoin
    Когда поток вызывает методы ожидания (например, Thread.Sleep(), Thread.Join() или ожидает сигнал от синхронизирующего примитива, он переходит в состояние WaitSleepJoin. Это временное состояние, отражающее блокировку потока.

  • Suspended
    Поток может быть приостановлен (хотя методы Suspend/Resume являются устаревшими и не рекомендуются к использованию). Состояние Suspended означает, что выполнение потока намеренно приостановлено.

  • AbortRequested / Aborted
    При вызове Abort() потоку устанавливается флаг AbortRequested, а затем, когда поток действительно прерывается, он переходит в состояние Aborted. Использование Abort() нежелательно, так как оно может оставить приложение в непредсказуемом состоянии.

  • Stopped
    Когда поток завершает выполнение, он получает состояние Stopped, что означает окончание его жизненного цикла.

Рассмотрим пример отслеживания статуса потока от создания до завершения:


static void Main()  
{  
Thread worker = new Thread(() =>  
{  
     Console.WriteLine("Поток начал выполнение.");  
     // Поток переходит в состояние WaitSleepJoin на время сна  
     Thread.Sleep(1500);  
     Console.WriteLine("Поток завершает работу.");  
});  
 
// До старта поток в состоянии Unstarted  
Console.WriteLine("Состояние потока до старта: " + worker.ThreadState);  
worker.Start();  
 
// Периодически выводим текущее состояние потока  
while (worker.IsAlive)  
{  
     Console.WriteLine("Текущее состояние потока: " + worker.ThreadState);  
     Thread.Sleep(300);  
}  
// После завершения поток получает состояние Stopped  
Console.WriteLine("Состояние потока после завершения: " + worker.ThreadState);  
}

 Вывод программы:

Состояние потока до старта: Unstarted
Текущее состояние потока: Running
Поток начал выполнение.
Текущее состояние потока: WaitSleepJoin
Текущее состояние потока: WaitSleepJoin
Текущее состояние потока: WaitSleepJoin
Текущее состояние потока: WaitSleepJoin
Поток завершает работу.
Состояние потока после завершения: Stopped

В качестве эксперимента попробуйте вызвать методы Suspend и Abort во время выполнения потока.

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

.NET позволяет управлять приоритетами потоков при их создании. Приоритет отвечает за распределение ресурсов на поток и время взятия этого потока в работу планировщиком ОС. На поток с более высоким приоритетом будет выделено больше ресурсов, он распределится на ядро быстрее.

Приоритеты потоков — это подсказка для планировщика ОС, и они не гарантируют абсолютного распределения ресурсов. Неправильное использование приоритетов может привести к неравномерному распределению процессорного времени.

Для управления приоритетами потоков используется перечисление ThreadPriority, которое содержит следующие значения:

  • HighestПоток с этим приоритетом получает наибольшее количество процессорного времени. Используется для задач, требующих максимально быстрого отклика.

  • AboveNormalПоток получает чуть больше процессорного времени, чем поток с нормальным приоритетом. Полезно для задач, которым нужна небольшая дополнительная производительность.

  • NormalЭто значение используется по умолчанию. Поток получает стандартное количество процессорного времени, сбалансированное с другими потоками.

  • BelowNormalПоток получает немного меньше процессорного времени, чем потоки с нормальным приоритетом. Подходит для фоновых задач, выполнение которых не критично.

  • LowestПоток с самым низким приоритетом. Используется для задач, которые могут выполняться в фоновом режиме и не требуют быстрой реакции.

Пример кода:


static void Main()  
{  
Thread lowPriorityThread = new Thread(() => Compute("LowPriority"));  
Thread highPriorityThread = new Thread(() => Compute("HighPriority"));  
 
lowPriorityThread.Priority = ThreadPriority.Lowest;  
highPriorityThread.Priority = ThreadPriority.Highest;  
 
lowPriorityThread.Start();  
highPriorityThread.Start();  
}  
 
static void Compute(string threadName)  
{  
long count = 0;  
for (long i = 0; i < 1_000_000_000; i++)  
{  
     count++;  
}  
Console.WriteLine($"Поток '{threadName}' завершил выполнение.");  
}

В этом примере создается два потока с разными приоритетами. Скорее всего, поток с высоким приоритетом завершится быстрее, и при запуске программы вывод будет следующий:

Поток 'HighPriority' завершил выполнение.
Поток 'LowPriority' завершил выполнение.

Заключение

В этой статье мы разобрали класс Thread в C# и его основные методы. Этот класс позволяет гибко управлять потоками — от времени старта до конкретного приоритета, что позволяет контролировать параллелизацию выполнения задач.

Спасибо за прочтение статьи! Также я веду телеграм-канал Flexible Coding, где рассказываю про свой опыт в программировании.

вАЙТИ — DIY-медиа для ИТ-специалистов. Делитесь личными историями про решение самых разных ИТ-задач и получайте вознаграждение.


Еще статьи по теме #Разработка

Многопоточность. Снизу вверх. ОС
Вторая статья из цикла для разработчиков, которые хотят узнать больше о потоках

Многопоточность. Снизу вверх. Процессор
Как работают потоки в многоядерных процессорах

Основные методики сбора и фиксации требований аналитиком
Пять способов глубже понять задачу и пожелания заказчика

Расширяем свой инструментарий React-приложения
Несколько полезных типовых хуков

Как мы увеличили скорость загрузки сайта и подняли трафик на 54%
Пример из моей практики и пара советов

Теги:
Хабы:
+7
Комментарии12

Публикации

Информация

Сайт
cloud.beeline.ru
Дата регистрации
Дата основания
Численность
501–1 000 человек
Местоположение
Россия