Содержание
Одним из ключевых моментов программирования является многопоточность. Поток (thread) — это некий путь выполнения кода. Даже, если мы создаем обычное консольное приложение типа «Hello world», то создается, как минимум один (он же главный) поток, который начинает свой путь с метода Main
. Каждому потоку в приложении выделяется определенное количество времени (квант) в течение которого поток выполняет возложенную на него задачу. И, даже, если компьютер оснащен одноядерным процессором мы можем создавать несколько потоков в приложении, однако это будет, скорее, псевдо-многопоточность в рамках которой потоки просто будут очень быстро переключаться между собой, обеспечивая тем самым эффект многопоточности.
Однопоточность vs Многопоточность
Представим, что наша программа должна выполнить несколько различных задач, например, отсортировать большой массив (Задача 1), рассчитать факториал (Задача 2) и отправить электронное письмо (Задача 3). Когда мы используем однопоточную модель, то наша программа будет работать следующим образом:
Не важно в каком порядке будут выполняться задачи — начнется ли сначала выполнение первой задачи или третьей. В любом случае, следующая задача не будет выполнятся, пока не выполнится предыдущая и для этого будет задействован один поток. Если же мы будем использовать многопоточную модель, то программа будет работать следующим образом:
Здесь под каждую задачу мы можем выделить свой отдельный поток. В результате задачи выполняются так, что не тормозят работу друг друга, программа становится более отзывчивой на действия пользователя и т.д.
Когда может потребоваться многопоточность
Вариантов, когда можно использовать многопоточную модель в приложении можно привести несколько. Например, самый очевидный — ожидание ответа от какого-либо сервера в сети. Если использовать один поток в рамках которого приложение будет ожидать ответ от сервера, а затем выполнять другие задачи, то, если ответ будет идти достаточно долго, то программа будет «притормаживать». Намного хуже будет в том случае, если сервер вообще не ответит — в этом случае программа просто «зависнет» и не двинется дальше до полной перезагрузки. В этом случае, имеет смысл вынести работу с сервером в отдельный поток.
Другой пример — работа MS Word. Как известно, Word при наборе текста автоматически проверяет правописание и, в случае нахождения ошибок подчеркивает их. Если бы в Word использовалась однопоточная модель, то вряд ли этот текстовый редактор имел бы большую популярность. Проверка правописания и прочие «фишки» Word выполняются в отдельных потоках, не нарушая и не притормаживая при этом нашу непосредственную работу в редакторе.
Класс Thread
Основные типы и классы для работы с многопоточностью в C# располагаются в пространстве имен System.Threading
и главным из классов является класс Thread
. Именно благодаря Thread
мы можем создавать новые потоки в нашем приложении и делегировать им выполнение каких-либо задач, а использование статических методов этого класса позволяет управлять уже существующими потоками приложения.
Свойства класса Thread
Ниже в таблице представлены основные свойства класса Thread
.
Свойство | Описание |
Current |
Возвращает выполняющийся в данный момент поток. |
Execution |
Возвращает объект ExecutionContext , содержащий сведения о различных контекстах текущего потока. |
Is |
Возвращает значение, показывающее статус выполнения текущего потока. |
Is |
Возвращает или задает значение, показывающее, является ли поток фоновым. |
Is |
Возвращает значение, показывающее, принадлежит ли поток к группе управляемых потоков (пулу потоков). |
Managed |
Возвращает уникальный идентификатор текущего управляемого потока. |
Name |
Получает или задает имя потока. |
Priority |
Возвращает или задает значение, указывающее на планируемый приоритет потока. |
Thread |
Возвращает значение, содержащее состояние текущего потока. |
Посмотрим, какие значения будут содержать эти свойства для главного потока приложения типа «Hello world»:
Thread thread = Thread.CurrentThread; Console.WriteLine($"Name {thread.Name}"); thread.Name = "Главный поток приложения"; Console.WriteLine($"Name {thread.Name}"); Console.WriteLine($"ManagedThreadId {thread.ManagedThreadId}"); Console.WriteLine($"ThreadState {thread.ThreadState}"); Console.WriteLine($"IsAlive {thread.IsAlive}"); Console.WriteLine($"IsBackground {thread.IsBackground}"); Console.WriteLine($"IsThreadPoolThread {thread.IsThreadPoolThread}"); Console.WriteLine($"Priority {thread.Priority}");
Результат выполнения:
Name Главный поток приложения
ManagedThreadId 1
ThreadState Running
IsAlive True
IsBackground False
IsThreadPoolThread False
Priority Normal
Как можно видеть по итогу работы программы, в начале главный поток не имеет никакого имени, однако, мы можем его назначить, используя свойство Name
. Что касается статуса (состояния) потока, то свойство ThreadState
может принимать следующие значения:
Aborted |
256 | Поток не выполняет работу, но его состояние еще не изменилось на Stopped . (включает в себя значение AbortRequested ) |
AbortRequested |
128 | Для потока был вызван метод Abort(Object) , но поток еще не получил исключение ThreadAbortException , которое попытается завершить его. |
Background |
4 | Поток выполняется как фоновый |
Running |
0 | Поток был запущен, но не останавливался. |
Stopped |
16 | Поток был остановлен. |
StopRequested |
1 | Поток получает запрос на остановку. |
Suspended |
64 | Поток был приостановлен. |
SuspendRequested |
2 | Запрашивается приостановка работы потока. |
Unstarted |
8 | Метод Start() не был вызван для потока. |
WaitSleepJoin |
32 | Поток заблокирован. |
Состояние потока может многократно меняться в процессе выполнения приложения. По умолчанию, состояние потока Unstarted
, после запуска потока методом Start()
его статус изменяется на Running
, вызвав метод Sleep()
мы, тем самым, переведем поток в статус WaitSleepJoin
и т.д.
Что касается приоритета потока (свойство Priority
), то здесь возможны следующие варианты:
AboveNormal |
3 | Выполнение потока Thread может быть запланировано после выполнения потоков с приоритетом Highest и до потоков с приоритетом Normal . |
BelowNormal |
1 | Выполнение потока Thread может быть запланировано после выполнения потоков с приоритетом Normal и до потоков с приоритетом Lowest . |
Highest |
4 | Выполнение потока Thread может быть запланировано до выполнения потоков с любыми другими приоритетами. |
Lowest |
0 | Выполнение потока Thread может быть запланировано после выполнения потоков с любыми другими приоритетами. |
Normal |
2 | Выполнение потока Thread может быть запланировано после выполнения потоков с приоритетом AboveNormal и до потоков с приоритетом BelowNormal . По умолчанию потоки имеют приоритет Normal . |
По умолчанию, потоку устанавливается приоритет Normal
, однако, мы можем изменить его и назначить другое значение.
Пример потока
В качестве заключения к вводной части, напишем простенькое приложение, демонстрирующее работу двух потоков:
Thread thread = new Thread(new ThreadStart(Proc)); thread.Start(); string s; do { s = Console.ReadLine(); Console.WriteLine(s); } while (s != "q"); static void Proc() { for (int i = 0; i < 10; i++) { Console.WriteLine($"Это {i} итерация цикла в потоке"); Thread.Sleep(1000); } }
Здесь мы создаем и запускаем поток thread
. Этот поток 10 раз выводит в консоль сообщение. Главный же поток работает до тех пор, пока мы не введем в консоль символ q
и не нажмем Enter
. Это пример, не имеющий какой-либо практической ценности, однако он прекрасно демонстрирует, во-первых, работу двух потоков в приложении (главного и вторичного), а, во-вторых, как приложение продолжает работу даже в том случае, если главный поток прекращает свою работу. Попробуйте запустить приложение, сразу написать q
и нажать Enter — после этого вы уже не сможете ничего писать в консоль,так как главный поток прекратит работу, однако вторичный (созданный нами) поток продолжит работу и приложение завершит свою работу только после того, как вторичный поток отработает все 10 итераций цикла.
Секрет такого поведения кроется в свойстве Is
. Дело в том, что все потоки, которые мы создаем в приложении, по умолчанию создаются с приоритетом Normal
и значением IsBackground
равным false
, т.е. как потоки переднего плана. И до тех пор, пока в приложении есть хотя бы один поток переднего плана, не завершивший задачу, приложение будет работать. Попробуем создать наш поток как фоновый:
Thread thread = new Thread(new ThreadStart(Proc)); thread.IsBackground = true; thread.Start(); string s; do { s = Console.ReadLine(); Console.WriteLine(s); } while (s != "q"); static void Proc() { for (int i = 0; i < 10; i++) { Console.WriteLine($"Это {i} итерация цикла в потоке"); Thread.Sleep(1000); } }
Теперь, если запустить приложение и набрать q
, то приложение сразу же прекратит свою работу вне зависимости от того прошел ли вторичный поток 10 итераций цикла или нет.
Итого
Сегодня мы рассмотрели основные свойства класса Thread
и написали небольшое приложение, демонстрирующее работу двух потоков в приложении. На этом тема многопоточности в C#, конечно же, не исчерпывается и в следующий раз мы более подробно рассмотрим класс Thread
.