В данной статье мы расскажем, что такое потоки в C#, приоритеты потоков и их типы, покажем, как работать с потоками и как ими управлять, создадим несколько наглядных примеров, объясняющих их работу.
Оглавление:
Что такое потоки в C#
Реализация потоков в C#
— Как создавать потоки в C#
— Как запускать потоки в C#
— Как приостановить потоки в C#
— Приоритеты потоков в C#
— Изменение типов потоков в C#
Примеры работы потоков в C#
— Программа №1: как работают потоки в C#
— Программа №2: приоритеты потоков в C#
— Программа №3: типы потоков в C#
Что такое потоки в C#
Если говорить простым языком, то поток — это некая независимая последовательность инструкций для выполнения того или иного действия в программе. В одном конкретном потоке выполняется одна конкретная последовательность действий.
Совокупность таких потоков, выполняемых в программе параллельно называется многопоточностью программы.
Следует также запомнить, что в действительности потоки выполняются всё-таки не совсем параллельно. Дело в том, что процессор физически не может обрабатывать параллельно несколько инструкций или процессов. Однако его вычислительной мощи хватает настолько, что он может выполнять все операции по небольшому фрагменту по очереди, отводя на каждый такой фрагмент по очень маленькому кусочку времени, настолько, что кажется, будто все процессы в компьютере выполняются параллельно.
Точно такая же ситуация происходит и с потоками. Если в программе имеется 3 потока, то сначала выполняется кусочек кода из одного потока, потом кусочек кода из другого, затем — из третьего, после чего процессор снова переходит к какому-либо из двух других потоков. Выбор, какой поток необходимо назначить для выполнения в данный момент остаётся за процессором. Происходит это в доли миллисекунд, поэтому происходит ощущение параллельной работы потоков.
Стандартно в проектах Visual Studio существует только один основной поток — в методе Main. Всё, что в нём выполняется — выполняется последовательно строка за строкой. Но при необходимости можно «распараллелить» выполняемые процессы при помощи потоков.
Для лучшего осознания можно представить следующий пример. Допустим, что наша программа и данные, которые в ней содержатся — это офис с различными предметами (папками, столами, стульями, ручками), а потоки — это работники данного офиса (изначально у нас только один работник, выполняющий всю работу), и каждый работник занимается теми делами, которые ему было сказано выполнять. Работники могут выполнять одинаковые задания, а могут и различные. В случае выполнения какой-либо одной задачи, несколько работников справятся быстрее, чем один.
Например, если один работник будет собирать шкаф час, то вдвоём они могут управиться уже за полчаса. Однако не стоит переусердствовать в количестве работников (потоков). Математически, если нанять 4 работника, то шкаф соберется за 15 минут, если нанять 60 работников — за 1 минуту, а если нанять 3600, то вообще за секунду, но ведь на деле это неверно. Работники будут только мешать друг другу, толкаться, отнимать друг у друга детали, и процесс сборки шкафа может затянуться очень надолго.
Так же и с потоками. Чем больше потоков, тем выше вероятность, что они будут мешать друг другу выполнять свою работу. Например, если заставить работать огромное количество потоков с одними и теми же данными, потокам придётся выстраиваться в очередь для их обработки (например, если тем же 3600 рабочим дать какое-либо письменное задание, но предоставить им для этого дела всего одну ручку, то работникам, естественно, придётся становиться друг за другом в очередь за ручкой, чтобы после её получения выполнить поставленную задачу. Времени это займёт довольно много).
Итог: потоки надо распределять с умом и исключительно в случаях, когда это действительно необходимо для ускорения работы программы либо для повышения производительности.
Язык C# имеет встроенную поддержку многопоточности, а среда .NET Framework предоставляет сразу несколько классов для работы с потоками, что в купе очень помогает гибко и правильно реализовывать и настраивать многопоточность в проектах.
В среде .NET Framework существует два типа потоков: основной и фоновый (вспомогательный). В целом отличие между ними одно — если первым завершится основной поток, то фоновые потоки в его процессе будут также принудительно остановлены, если же первым завершится фоновый поток, то это не повлияет на остановку основного потока — тот будет продолжать функционировать до тех пор, пока не выполнит всю работу и самостоятельно не остановится. Обычно при создании потока ему по-умолчанию присваивается основной тип. О том, как узнать, к какой разновидности относится тот или иной поток, как придать потоку нужный тип, что такое приоритеты и как их устанавливать, и как в целом работать с потоками в C# мы поговорим ниже.
Реализация потоков в C#
Как создавать потоки в C#
Перво-наперво для работы с потоками в C# необходимо подключить специальную директиву:
1 |
using System.Threading; |
Именно она позволяет нам реализовывать необходимые потоки и их настройку.
Дальше стоит понять, что любой поток в C# должен обязательно происходить в каком-либо методе или функции, поэтому для работы с новым потоком необходимо сначала создать для него, например, метод, который и будет точкой входа для этого потока.
Для примера создадим пустой метод под именем fornewthread, который ничего не возвращает:
1 2 3 4 |
static void fornewthread() { //в действительности здесь должны находиться инструкции, которые будут выполняться в нашем новом потоке } |
Теперь мы можем создать сам поток (например, в главном методе Main). Назовём его mythread:
1 |
Thread mythread = new Thread(fornewthread); |
Как вы видите, для создания потока нужно вызвать делегат Thread, а также передать конструктору адрес метода (в скобках).
Как запускать потоки в C#
Потоки в C# начинают выполняться не сразу после их инициализации. Каждый из созданных потоков необходимо сначала запустить. Делается это следующим образом: имя_потока.Start();. В случае из нашего примера строка запуска будет такой:
1 |
mythread.Start(); |
Мы записали данный код, также как и предыдущий, в методе Main сразу после инициализации самого потока. Теперь, при запуске программы, как только в Main выполнится метод Start(), начнёт работать вновь созданный поток. После того, как выполнился Start(), работа программы «распараллеливается»: один поток начинает выполнять код из метода fornewthread, а второй поток продолжает выполнять операции, которые остались ещё не выполненными в методе Main (после Start()). Естественно, это всё произойдёт, если в fornewthread или в Main имеется дополнительный код. Если же один из потоков выполнит работу раньше другого, то первый будет ожидать окончания выполнения работы последнего. А делать это он будет по причине, описанной выше: так как оба потока являются основными, то завершение какого-либо одного потока не влияет на завершение другого. Если бы один из наших потоков был фоновым и «задержался» бы с работой, то при окончании работы основного потока, принудительно завершился бы и он.
Как приостановить потоки в C#
Иногда бывают такие сценарии, когда необходимо бывает приостановить поток, например для того, чтобы пропустить другие потоки и не мешать им выполнять свою работу, либо для снижения потребления процессорного времени.
Есть несколько способов остановки потоков, но в данной статье мы рассмотрим наиболее употребляемый:
1 |
Thread.Sleep(100); |
В этой строке мы указали, что поток, к которому будет относиться данная инструкция, будет приостановлен на 100 миллисекунд. Как нетрудно догадаться после такого объяснения, значение в скобках указывает, на какой период времени в миллисекундах будет приостановлен поток. Как только заданное время пройдёт, поток снова возобновит свою работу и продолжит с места, на котором остановился. Кстати говоря, число, заданное в скобках — это количество миллисекунд для задержки в идеале, на практике поток может возобновиться на несколько миллисекунд позже или раньше, это зависит не только от программы, но и от характеристик операционной системы.
В метод Sleep также можно передать и значение ноль:
1 |
Thread.Sleep(0); |
В таком случае поток приостановится на положенный ему интервал времени (мы же помним, что все потоки выполняются по кусочку и по очереди, и при этом на каждый такой кусочек выделяется очень небольшое количество времени, но достаточное для того, чтобы выполнить некоторое количество действий), который он «отдаёт» любому другому готовому к выполнению потоку с таким же приоритетом, как и у него (про приоритеты поговорим ниже). Если же не удастся найти ни один такой «ожидающий» поток, то выполнение текущего потока не будет приостановлено, и он продолжит свою работу.
Приоритеты потоков в C#
Когда в программе фигурирует несколько потоков, выбор процессором следующего потока для выполнения не является рандомным. Дело в том, что у каждого потока имеется значение приоритета, чем выше приоритет потока, тем важнее для процессора предоставить время и ресурсы для выполнения именно ему. Если же приоритет потока не слишком высокий, значит он может и подождать в очереди, пока выполняются более приоритетные потоки.
Всего существует пять вариантов приоритетов потоков в C#:
- Highest — самый высокий
- AboveNormal — выше среднего
- Normal — стандартный
- BelowNormal — ниже среднего
- Lowest — самый низкий
Как уже было сказано, чем выше приоритет, тем быстрее он выполнится, опережая потоки с меньшим значением. Если у потоков одинаковые приоритеты, они выполняются по очереди.
По умолчанию все создаваемые потоки в C# имеют стандартный приоритет Normal. Однако приоритеты потоков можно и менять, делается это так:
имяпотока.Priority = ThreadPriority.вариантприоритета;
Допустим мы решили присвоить описанному выше потоку mythread самый высокий приоритет. В таком случае нам надо записать (где-нибудь перед методом Start):
1 |
mythread.Priority = ThreadPriority.Highest; |
Если же решим присвоить приоритет чуть ниже, чем стандартный, то, соответственно строка будет такой:
1 |
mythread.Priority = ThreadPriority.BelowNormal; |
Тогда выполнение данного потока будет происходить после выполнения потоков с более высоким приоритетом.
Примечание: приоритет метода Main изменить нельзя. Он всегда будет в позиции Normal.
Простую программу, показывающую на практике необходимость ввода приоритетов можно будет посмотреть в соответствующем разделе ниже.
Изменение типов потоков в C#
Как мы рассказывали выше, потоки в C# бывают основными и фоновыми. Также мы говорили о том, что при создании потока он по умолчанию становится основным. Однако его тип можно, также как и приоритет, поменять. Для того, чтобы сделать основной поток фоновым, нужно изменить его свойство IsBackground: имя_потока.IsBackground = true;
1 |
mythread.IsBackground = true; |
Если установить данное свойство в значение true, то поток будет работать как фоновый, если в значение false — как основной.
Однако, если в программе используется несколько потоков, то не всегда можно понять, какой поток к какому типу относится. Но, к счастью, всегда можно легко это узнать:
1 2 |
bool a = mythread.IsBackground; Console.WriteLine(a); |
Надо всего-навсего присвоить переменной типа bool значение данного свойства, и, если необходимо, вывести полученное значение на экран. Суть такая же: если выведется true, значит поток фоновый, иначе — основной.
Примеры работы потоков в C#
В данном разделе мы на практике увидим, как работают потоки в C# и создадим для наглядного примера пару простеньких программок.
Программа №1: как работают потоки в C#
В первой программе мы просто реализуем работу нескольких параллельных потоков и особое внимание обратим на выходные данные. Для примера создадим проект C# с тремя потоками, каждый из которых будет выводить числа от 0 до 9.
Вот его код:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
static void mythread1() { for (int i = 0; i < 10; i++) { Console.WriteLine("Поток 1 выводит " + i); } } static void mythread2() { for (int i = 0; i < 10; i++) { Console.WriteLine("Поток 2 выводит " + i); } } static void Main(string[] args) { Thread thread1 = new Thread(mythread1); Thread thread2 = new Thread(mythread2); thread1.Start(); thread2.Start(); for (int i = 0; i < 10; i++) { Console.WriteLine("Поток 3 выводит " + i); } Console.ReadLine(); } |
Итак, как мы помним, для того, чтобы инициализировать потоки в C#, нам необходимы методы, в которых они будут выполняться.
Поэтому мы создали два дополнительных метода mythread1 и mythread2, в которых будут выполняться соответственно потоки thread1 и thread2. Третий поток у нас будет работать непосредственно в методе Main.
Мы инициализировали все потоки (третий поток заработал с самого запуска программы, его не надо инициализировать и начинать, естественно), затем запускаем их. В каждой функции происходит одно и то же действие — вывод в консоль цифр от 0 до 9, и при этом каждый поток получил от нас нумерацию от 1 до 3 для удобства восприятия.
Пора запустить программу и посмотреть, что она нам выведет:
На примере данного скриншота мы можем увидеть «последовательность» многопоточности. Как уже несколько раз было сказано выше, процессор выполняет по кусочку от одного потока, затем кусочек от другого, потом от третьего и так далее.
Здесь мы видим, что процессор сначала предоставил работу третьему потоку (тот вывел первые 7 цифры), затем перешёл к первому потоку (вывел все 10 цифр и прекратил работу), потом выделил ресурсы для второго (успел вывести 9 цифр), затем опять вернул управление третьему потоку (вывел оставшиеся 3 цифры, прекратил работу), а потом опять вернулся ко второму (вывел последнюю цифру). Если мы запустим программу ещё раз, то распределение ресурсов среди потоков может уже измениться и вывод будет происходить как-нибудь иначе, в третий раз — опять по-другому (стоит заметить, что такой «рандом» возможен во многом потому, что все три потока имеют один приоритет — Normal, и, кроме того, все они являются основными потоками). Также стоит заметить, что с самого начала работы программы начинает выполняться поток в методе Main, поэтому он будет чаще других фигурировать на первых местах, несмотря на то, что по коду он запускает цикл последним. Процессор за отведенное на данный поток время обычно успевает не только запустить остальные потоки, но и также вывести несколько цифр (или даже все), прежде чем отведенный для третьего потока промежуток времени закончится.
Для примера мы ещё три раза запустим программу и при каждом выполнении зафиксируем полученные данные:
Как и было сказано, каждый раз ресурсы, разделяемые процессором между потоками меняются.
Вы также можете скачать исходный код данной программы и сами попробовать проанализировать работу потоков:
Теперь давайте на примере этой же программы попробуем поэкспериментировать с приоритетами и типами потоков.
Программа №2: приоритеты потоков в C#
Теперь давайте посмотрим, на что влияет приоритетность потоков в C#. Мы решили взять для примера программу с тремя потоками, каждый из которых будет выводить в консоль цифры от 0 до 9, от 10 до 19 и от 20 до 29 соответственно. Поставим перед собой задачу вывести в консоль все эти числа последовательно от 0 до 29.
Если мы пренебрежем приоритетами при постановке данной задачи, то у нас никак не получится вывести все наши числа по очереди. Так как у всех трёх потоков одинаковый приоритет, процессору, по сути, будет всё равно, какой за каким потоки выводить, и у нас частенько будет выходить плохо отсортированный набор чисел, примерно такой же, как в программе выше, а именно:
Попробуем реализовать всё так, чтобы у нас получился необходимый нам результат.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
static void mythread1() { for (int i = 0; i < 10; i++) { Console.WriteLine("Поток 1 выводит " + i); } } static void mythread2() { for (int i = 10; i < 20; i++) { Console.WriteLine("Поток 2 выводит " + i); } } static void mythread3() { for (int i = 20; i < 30; i++) { Console.WriteLine("Поток 3 выводит " + i); } } static void Main(string[] args) { Thread thread1 = new Thread(mythread1); Thread thread2 = new Thread(mythread2); Thread thread3 = new Thread(mythread3); thread1.Priority = ThreadPriority.Highest; thread2.Priority = ThreadPriority.AboveNormal; thread3.Priority = ThreadPriority.Lowest; thread1.Start(); thread2.Start(); thread3.Start(); Console.ReadLine(); } } |
У нас имеется три потока и три метода, в которых они выполняются. Первый метод выводит на экран числа от 0 до 9, второй — от 10 до 19, третий — с 20 по 29.
В методе Main мы задаём приоритеты потокам. Так как нам надо сначала вывести числа из первого потока, мы устанавливаем ему самый высокий приоритет. Следующий за ним второй поток имеет приоритет чуть ниже, поэтому будет выполняться вторым, третий поток имеет самый низкий приоритет, поэтому будет выполняться после всех остальных приоритетов.
Далее мы запускаем все три потока и смотрим, что же у нас получается:
Вот и всё! Наши числа вывелись в правильном порядке, и всё благодаря приоритетам.
Также можно поиграться с изменением значения приоритетов потоков. Так, например, можно поменять приоритеты на такие:
1 2 3 |
thread1.Priority = ThreadPriority.Lowest; thread2.Priority = ThreadPriority.BelowNormal; thread3.Priority = ThreadPriority.Highest; |
И тогда мы увидим в консоли вот такой вывод:
Поэкспериментировать с приоритетами можно на примере нашей программы, скачав её по ссылке ниже.
Скачать исходник
Программа №3: типы потоков в C#
Как мы рассказывали выше, потоки в C# бывают приоритетными и фоновыми. Разница между ними в том, что если основной поток будет завершен, то и вложенные в него фоновые потоки также будут завершены принудительно. Если же фоновый поток завершится раньше основного, то на основной поток это не повлияет, и он продолжит свою работу.
Мы также описывали выше, как поменять тип потока в программе, как как по умолчанию все потоки создаются приоритетными. Теперь давайте на практике посмотрим, чем отличаются фоновые потоки в C# от основных.
Мы создали программу, которая проливает свет на влияние типов потоков. Код нашего приложения довольно небольшой:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
static void mythread1() { for (int i = 0; i < 1000000; i++) { Console.WriteLine("Поток 1 выводит " + i); } } static void Main(string[] args) { Thread thread1 = new Thread(mythread1); thread1.IsBackground = true; thread1.Start(); Thread.Sleep(100); } } |
Здесь у нас имеется два потока — thread1 и поток в из метода Main. Изначально они являются приоритетными, следовательно, работают независимо друг от друга, и, пока не закончится выполняться один поток, второй поток нельзя будет закончить принудительно. Однако в нашей программе мы присвоили потоку thread1 другой тип — фоновый, в строке:
1 |
thread1.IsBackground = true; |
А так как данный поток вызывается в методе Main, значит он будет полностью зависеть от потока в этом методе.
Смысл нашей программы в следующем: поток thread1 вызывается из метода Main и начинает работу в методе mythread1, который должен выводить на экран числа от 0 до 999999. Однако загвоздка в том, что практически после старта работа метода Main прекращается (заметьте, у нас нет строки Console.ReadLine(); в конце Main). Поток thread1 никак не успеет вывести все 999999 чисел за столь короткий промежуток времени, но так как в нашей программе он является фоновым, то принудительно завершится вместе с завершением потока в методе Main.
Мы также добавили строку:
1 |
Thread.Sleep(100); |
Как мы помним, данный метод приостанавливает работу потока на время, указанное скобках (миллисекунды). В нашем случае мы приостанавливаем приоритетный поток исключительно для того, чтобы успеть сделать скриншот вывода (тоже нелегкое дело, успеть сфотографировать экран за одну десятую секунды, но если сделать остановку значительно дольше, то поток thread1 успеет вывести все числа).
Итак, после запуска программы и выжидания 100 мс у нас в консоли следующее:
Это последние числа, которые успел вывести поток thread1 перед завершением выполнения потока в Main, затем консоль закрывается, и приложение завершает свою работу.
Для дополнительного примера можно убрать или заменить строку thread1.IsBackground = true; (тогда данный поток снова станет приоритетным), удалить Thread.Sleep(100), добавить в конец метода Main строку Console.ReadLine() и снова запустить данную программу. Перед вами начнут проплывать огромное количество чисел, но вы не сможете остановить это действие посредством работы потока в Main (иными словами, нажатием любой кнопки), пока поток thread1 не выполнится полностью и не выведет все 999999 чисел.
Вот в этом и заключается разница между основными и фоновыми потоками в языке C#. Нашу программу (с двумя вариантами выполнения) можно скачать по ссылке ниже, ну а на этом наша статья, посвященная работе потоков в C# заканчивается.
Свои вопросы и пожелания можете оставить в комментариях, спасибо за прочтение!
Скачать исходникПоделиться в соц. сетях:
Спасибо, очень интересная статья. Я теперь понял работу потоков. И самое главное, что написано простым и понятным языком.
Никита, спасибо Вам за отзыв!
Доброго времени суток, хотелось бы подробней узнать как например из одного потока, вывести информацию, например в TextBox
Здравствуйте! Посмотрите ниже комментарий (от 21.12.2016 в 12:53), там есть ответ на Ваш вопрос!
Здравствуйте! Очень хорошая статья. А вот мне интересно, я конечно думаю что можно, но все же, можно ли привязать время выполнения потока, к прогрессбару, если да, то тяжело ли это?
Добрый день, Александр! Ниже, в комментарии от 21.12.2016 в 12:53 написано, как из созданного потока обращаться к textBox. Точно также Вы можете передавать данные в прогрессбар.
Спасибо.
Пытаюсь на форме сделать кнопки СТАРТ и СТОП, отображать в ТекстБоксах изменения значений, натыкаюсь на ошибку, что переменные в этом потоке не доступны. Можно, как-то программно переключаться между потоками в одном и том же методе, я новичек?
Чтобы из потока обращаться к каким-либо элементам управления, нужно использовать Invoke. Допустим к textBox (Windows Forms):
private void button1_Click(object sender, EventArgs e)
{
Thread thread1 = new Thread(mythread1);
thread1.Start();
}
void mythread1()
{
this.textBox1.BeginInvoke((MethodInvoker)(() => this.textBox1.Text = «text»));
}
На WPF используйте такое выражение:
Dispatcher.BeginInvoke((Action)(() => this.textBox1.Text = «text»));
Спасибо!
Попробую так сделать.
Спасибо тебе, добрый человек! Наконец-то вменяемая статья о потоках!
Присоединяюсь к комментарию Евгения.
Да автор молодец, статья простым языком изложена и очень полезная!
Ну наконец то хоть кто-то понятно объяснил про потоки.
Админ ты самый лучший на земле человек, огромное тебе человеческое спасибо, я даже закладку добавил) Это просто огонь)
Автор спасибо, статья облегчила жизнь.
Большое спасибо. отличная статья