Foreversoft.ru

IT Справочник
1 просмотров
Рейтинг статьи
1 звезда2 звезды3 звезды4 звезды5 звезд
Загрузка...

Потоки си шарп

Программирование на C, C# и Java

Уроки программирования, алгоритмы, статьи, исходники, примеры программ и полезные советы

ОСТОРОЖНО МОШЕННИКИ! В последнее время в социальных сетях участились случаи предложения помощи в написании программ от лиц, прикрывающихся сайтом vscode.ru. Мы никогда не пишем первыми и не размещаем никакие материалы в посторонних группах ВК. Для связи с нами используйте исключительно эти контакты: vscoderu@yandex.ru, https://vk.com/vscode

Потоки в C# для начинающих: разбор, реализация, примеры

В данной статье мы расскажем, что такое потоки в C#, приоритеты потоков и их типы, покажем, как работать с потоками и как ими управлять, создадим несколько наглядных примеров, объясняющих их работу.

Что такое потоки в C#

Если говорить простым языком, то поток – это некая независимая последовательность инструкций для выполнения того или иного действия в программе. В одном конкретном потоке выполняется одна конкретная последовательность действий.
Совокупность таких потоков, выполняемых в программе параллельно называется многопоточностью программы.

Следует также запомнить, что в действительности потоки выполняются всё-таки не совсем параллельно. Дело в том, что процессор физически не может обрабатывать параллельно несколько инструкций или процессов. Однако его вычислительной мощи хватает настолько, что он может выполнять все операции по небольшому фрагменту по очереди, отводя на каждый такой фрагмент по очень маленькому кусочку времени, настолько, что кажется, будто все процессы в компьютере выполняются параллельно.

Точно такая же ситуация происходит и с потоками. Если в программе имеется 3 потока, то сначала выполняется кусочек кода из одного потока, потом кусочек кода из другого, затем – из третьего, после чего процессор снова переходит к какому-либо из двух других потоков. Выбор, какой поток необходимо назначить для выполнения в данный момент остаётся за процессором. Происходит это в доли миллисекунд, поэтому происходит ощущение параллельной работы потоков.

Стандартно в проектах Visual Studio существует только один основной поток – в методе Main. Всё, что в нём выполняется – выполняется последовательно строка за строкой. Но при необходимости можно “распараллелить” выполняемые процессы при помощи потоков.

Для лучшего осознания можно представить следующий пример. Допустим, что наша программа и данные, которые в ней содержатся – это офис с различными предметами (папками, столами, стульями, ручками), а потоки – это работники данного офиса (изначально у нас только один работник, выполняющий всю работу), и каждый работник занимается теми делами, которые ему было сказано выполнять. Работники могут выполнять одинаковые задания, а могут и различные. В случае выполнения какой-либо одной задачи, несколько работников справятся быстрее, чем один.

Например, если один работник будет собирать шкаф час, то вдвоём они могут управиться уже за полчаса. Однако не стоит переусердствовать в количестве работников (потоков). Математически, если нанять 4 работника, то шкаф соберется за 15 минут, если нанять 60 работников – за 1 минуту, а если нанять 3600, то вообще за секунду, но ведь на деле это неверно. Работники будут только мешать друг другу, толкаться, отнимать друг у друга детали, и процесс сборки шкафа может затянуться очень надолго.

Так же и с потоками. Чем больше потоков, тем выше вероятность, что они будут мешать друг другу выполнять свою работу. Например, если заставить работать огромное количество потоков с одними и теми же данными, потокам придётся выстраиваться в очередь для их обработки (например, если тем же 3600 рабочим дать какое-либо письменное задание, но предоставить им для этого дела всего одну ручку, то работникам, естественно, придётся становиться друг за другом в очередь за ручкой, чтобы после её получения выполнить поставленную задачу. Времени это займёт довольно много).

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

Язык C# имеет встроенную поддержку многопоточности, а среда .NET Framework предоставляет сразу несколько классов для работы с потоками, что в купе очень помогает гибко и правильно реализовывать и настраивать многопоточность в проектах.

В среде .NET Framework существует два типа потоков: основной и фоновый (вспомогательный). В целом отличие между ними одно – если первым завершится основной поток, то фоновые потоки в его процессе будут также принудительно остановлены, если же первым завершится фоновый поток, то это не повлияет на остановку основного потока – тот будет продолжать функционировать до тех пор, пока не выполнит всю работу и самостоятельно не остановится. Обычно при создании потока ему по-умолчанию присваивается основной тип. О том, как узнать, к какой разновидности относится тот или иной поток, как придать потоку нужный тип, что такое приоритеты и как их устанавливать, и как в целом работать с потоками в C# мы поговорим ниже.

Реализация потоков в C#

Как создавать потоки в C#

Перво-наперво для работы с потоками в C# необходимо подключить специальную директиву:

Читать еще:  Strlen си реализация

Многопоточность

Введение в многопоточность. Класс Thread

Одним из ключевых аспектов в современном программировании является многопоточность . Ключевым понятием при работе с многоопоточностью является поток. Поток предствляет некоторую часть кода программы. При выполнении программы каждому потоку выделяется определенный квант времени. И при помощи многопоточности мы можем выделить в приложении несколько потоков, которые будут выполнять различные задачи одновременно. Если у нас, допустим, графическое приложение, которое посылает запрос к какому-нибудь серверу или считывает и обрабатывает огромный файл, то без многопоточности у нас бы блокировался графический интерфейс на время выполнения задачи. А благодаря потокам мы можем выделить отправку запроса или любую другую задачу, которая может долго обрабатываться, в отдельный поток. Поэтому, к примеру, клиент-серверные приложения (и не только они) практически не мыслимы без многопоточности.

Основной функционал для использования потоков в приложении сосредоточен в пространстве имен System.Threading . В нем определен класс, представляющий отдельный поток — класс Thread .

Класс Thread определяет ряд методов и свойств, которые позволяют управлять потоком и получать информацию о нем. Основные свойства класса:

Статическое свойство CurrentContext позволяет получить контекст, в котором выполняется поток

Статическое свойство CurrentThread возвращает ссылку на выполняемый поток

Свойство IsAlive указывает, работает ли поток в текущий момент

Свойство IsBackground указывает, является ли поток фоновым

Свойство Name содержит имя потока

Свойство Priority хранит приоритет потока — значение перечисления ThreadPriority

Свойство ThreadState возвращает состояние потока — одно из значений перечисления ThreadState

Некоторые методы класса Thread:

Статический метод GetDomain возвращает ссылку на домен приложения

Статический метод GetDomainID возвращает id домена приложения, в котором выполняется текущий поток

Статический метод Sleep останавливает поток на определенное количество миллисекунд

Метод Abort уведомляет среду CLR о том, что надо прекратить поток, однако прекращение работы потока происходит не сразу, а только тогда, когда это становится возможно. Для проверки завершенности потока следует опрашивать его свойство ThreadState

Метод Interrupt прерывает поток на некоторое время

Метод Join блокирует выполнение вызвавшего его потока до тех пор, пока не завершится поток, для которого был вызван данный метод

Метод Start запускает поток

Получение информации о потоке

Используем вышеописанные свойства и методы для получения информации о потоке:

В этом случае мы получим примерно следующий вывод:

Так как по умолчанию свойство Name у объектов Thread не установлено, то в первом случае мы получаем в качестве значения этого свойства пустую строку.

Статус потока

Статусы потока содержатся в перечислении ThreadState:

Aborted : поток остановлен, но пока еще окончательно не завершен

AbortRequested : для потока вызван метод Abort, но остановка потока еще не произошла

Background : поток выполняется в фоновом режиме

Running : поток запущен и работает (не приостановлен)

Stopped : поток завершен

StopRequested : поток получил запрос на остановку

Suspended : поток приостановлен

SuspendRequested : поток получил запрос на приостановку

Unstarted : поток еще не был запущен

WaitSleepJoin : поток заблокирован в результате действия методов Sleep или Join

В процессе работы потока его статус многократно может измениться под действием методов. Так, в самом начале еще до применения метода Start его статус имеет значение Unstarted . Запустив поток, мы изменим его статус на Running . Вызвав метод Sleep, статус изменится на WaitSleepJoin . А применяя метод Abort, мы тем самым переведем поток в состояние AbortRequested, а затем Aborted, после чего поток окончательно завершится.

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

Приоритеты потоков располагаются в перечислении ThreadPriority:

По умолчанию потоку задается значение Normal. Однако мы можем изменить приоритет в процессе работы программы. Например, повысить важность потока, установив приоритет Highest. Среда CLR будет считывать и анализировать значения приоритета и на их основании выделять данному потоку то или иное количество времени.

Использование потоков в Visual C# (Sharp)

На компьютерах под управлением операционной системы Windows задачи могут выполняться двумя способами: посредством потоков и посредством процессов. Процесс представляет собой исполнение программы, для которой операциоая система выделяет ресурсы, включающие, между прочих, и кванты времени. А поток — это легковесный процесс, исполняемый в контексте главного процесса.

Все процессы запускают, по крайней мере, один поток. Поток, запускаемый для процесса, является главным потоком, и при его завершении также завершается программа. Это означает, что если главный поток создает другие потоки, то по его завершению также завершаются и все созданные им потоки. С точки зрения архектуры при многозадачности главный поток запускает на исполнение и коордирует исполнение дочерних потоков. Два процесса не могут обращаться друг к другу. Поэтому сбой одного процесса не влияет на работу других исполняемых процессов. Но сбой в потоке, исполняемого в контексте процесса, может вызвать сбой всего процесса.

Дл я общения между процессами можно использовать механизм взаимодействия прессов. Но я бы не рекомендовал делать это, т. к. такой же эффект можно получить с помощь ю домен а приложения (AppDomain) .NET. Но в большинстве случаев прибать к домена м приложений нет надобности. Просто используйте легковесные (по сравнению с процессом) потоки, которыми легко управлять и которые просто прраммировать.

Читать еще:  Язык си натуральный логарифм

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

Поток можно создать таким образом, что он будет выполняться независимо от соавшего его потока. В следующем примере главный поток создает два дочерних потока, каждый из которых выводит текст в окно консоли. Обратите внимание на то, что тип Thread и другие родственные типы, используемые в примерах этой глы, находятся В пространстве имен System.Threading, т. е. в код необходимо добить строчку using System.Threading.

Thread threadl = new Thread(

threadl. Start () ,- thread2.Start();

Поток порождается созданием экземпляра типа Thread с последующим вызовом метода start О. Класс Thread содержит всю функциональность, необходимую для запуска и управления многозадачности.

При запуске потока, типу Thread требуется код для исполнения. Для этого примяется делегат Threadstart, который передается классу Thread через конструктор. В данном примере тип Threadstart не используется, т. к. в обоих потоках примяются программные конструктивы, не требующие объявления делегата. В первом потоке (threadl) используется анонимный метод, а во втором (thread2) — лямбдыражение. Вызов метода start () запускает исполнение потоком функциональнти анонимного метода или лямбда-выражения.

Результатом исполнения примера потоков может быть следующий вывод:

Well then goodbye hello there

Я говорю «может быть», потому ЧТО hello ВЫВОДИТСЯ перед goodbye. Такой вывод означает, что второй поток (thread2) начал исполняться перед первым потоком (threadl). Но последовательность вывода приветствия и прощания может также быть и в логическом порядке, что демонстрирует природу потоков — параллелость и причину, почему программирование потоков является таким трудным.

Представьте на секунду, что пример кода потоков выполнялся не параллельно, а последовательно. В таком случае метод threadl . star t о всегда вызывался бы первым, а метод thread2.start() — вторым, в результате чего мы бы всегда здовались перед прощанием. Последовательное выполнение не представляет для людей никаких проблем с пониманием. Но представить в уме взаимодействие нкольких задач одновременно — намного более сложная задача. В то время как выполнение параллельных потоков не является проблемой для компьютеров, логу этого выполнения программируют люди, более привычные к последовательному мышлению, и поэтому создаваемая ими параллельная логика может оказаться сержащей ошибки.

Разработку хорошо отлаженного потокового приложения можно сравнить с попыой собрать в одном месте несколько котов или собак. Если не соблюдать должную осторожность с потоками и их синхронизацией, тогда это будет подобно попытке загнать в один угол десять кошек, что почти невозможно, т. к. кошки не реагируют хорошо на команды. Но если быть осторожным и приложить должное усердие, тда это будет походить на сбор в одном углу десяти собак — задача достаточно легкая, если собаки выдрессированы.

Ожидание завершения исполняющегося потока

Вызов метода star t о запускает поток, таким образом, начиная выполнение зади. Код, вызвавший поток, не ожидает завершения исполнения созданного им пока, т. к. этот поток является полностью независимым от него. Поэтому при волнении пакетного процесса, нужно будет ждать, пока не завершатся все потоки. Запуск на выполнение нескольких потоков осуществляется с помощью вызываей логики. Вызывающая логика нужно только для запуска процессов, поэтому для ее работы требуется очень короткое время, по сравнению со временем для иолнения потоков. Но если завершить выполнение вызывающего потока, то также будет принудительно завершено выполнение всех его дочерних потоков, в резулате чего возможно искажение данных.

Чтобы этого не случилось, вызывающий поток должен знать, завершилось ли волнение созданного им потока. Для этого применяется метод Join о таким обром:

Thread thread = new Thread(

Вызов метода Join о в последней строчке кода блокирует вызывающий поток до тех пор, пока не завершится выполнение вызванного им потока. Метод Thread, sleep о переводит поток в состояние сна на указанное в параметре время, в данном примере на 2000 миллисекунд или 2 секунды.

Таким образом, проблема преждевременного завершения вызывающего потока решена, но какую пользу приносит ожидание вызывающим потоком завершения созданного потока? В данном примере использование метода Join () не приносит никакой пользы; здесь просто демонстрировалось его применение. Но если вызающий поток выполняет несколько потоков, то прежде чем продолжить свое иолнение, он должен дождаться завершения всех этих потоков. Поэтому в ситуии с несколькими выполняющимися потоками следует вызывать метод Join о абсолютно для всех этих потоков.

Вариантом метода join о является указание тайм-аута в параметре. Допустим, что для выполнения обработки запущенному потоку в худшем случае требуется самое большее 5 минут. Тогда, если в течение 5 минут поток не завершит выполнение самостоятельно, то мы завершаем его принудительно. Далее приводится соответсующий код:

Читать еще:  Scanf в си примеры

В примере вызов метода Joint) заставляет исполняемый поток ожидать в течение 300 000 миллисекунд (5 минут), прежде чем продолжить выполнение. В случае тайм-аута возвращается значение false, и выполнение потока принудительно зершается посредством метода Abort ().

Источник: Гросс К. С# 2008: Пер. с англ. — СПб.: БХВ-Петербург, 2009. — 576 е.: ил. — (Самоучитель)

Создание вторичных потоков

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

Создать метод, который будет точкой входа для нового потока.

Создать новый делегат ParametrizedThreadStart (или ThreadStart), передав конструктору адрес метода, определенного на предыдущем шаге.

Создать объект Thread, передав в качестве аргумента конструктора ParametrizedThreadStart/ThreadStart.

Установить начальные характеристики потока (имя, приоритет и т.п.).

Вызвать метод Thread.Start(). Это запустит поток на методе, который указан делегатом, созданным на втором шаге, как только это будет возможно.

Согласно второму шагу, можно использовать два разных типа делегатов для «указания» метода, который выполнит вторичный поток. Делегат ThreadStart относится к пространству имен System.Threading, начиная с .NET 1.0, и он может указывать на любой метод, не принимающий аргументов и ничего не возвращающий. Этот делегат пригодится, когда метод предназначен просто для запуска в фоновом режиме, без какого-либо дальнейшего взаимодействия.

Очевидное ограничение ThreadStart связано с невозможность передавать ему параметры для обработки. Тем не менее, тип делегата ParametrizedThreadStart позволяет передать единственный параметр типа System.Object. Учитывая, что с помощью System.Object представляется все, что угодно, можно передать любое количество параметров через специальный класс или структуру. Однако имейте в виду, что делегат ParametrizedThreadStart может указывать только на методы, возвращающие void.

Делегат ThreadStart

Чтобы проиллюстрировать процесс построения многопоточного приложения (а также его пользу), предположим, что есть консольное приложение, которое позволяет конечному пользователю выбирать, будет приложение выполнять свою работу в единственном первичном потоке либо распределит рабочую нагрузку на два отдельных потока выполнения.

После импортирования пространства имен System.Threading следующий шаг заключается в определении метода для выполнения работы (возможного) вторичного потока. Чтобы сосредоточиться на механизме построения многопоточных программ, этот метод будет просто выводить на консоль последовательность чисел, приостанавливаясь примерно на 2 секунды на каждом шаге. Ниже показано полное определение класса:

Внутри Main() сначала пользователю предлагается решить, сколько потоков применять для выполнения работы приложения: один или два. Если пользователь запрашивает один поток, нужно просто вызвать метод ThreadNumbers() внутри первичного потока. Если же пользователь отдает предпочтение двум потокам, необходимо создать делегат ThreadStart, указывающий на ThreadNumbers(), передать объект делегата конструктору нового объекта Thread и вызвать метод Start(), информируя среду CLR, что этот поток готов к обработке.

Если теперь запустить эту программу в одном потоке, обнаружится, что финальное окно сообщения не отображает сообщения, пока вся последовательность чисел не будет выведена на консоль. Поскольку после вывода каждого числа установлена пауза примерно в 2 секунды, это создаст не слишком приятное впечатление у пользователя. Однако в случае выбора двух потоков окно сообщения отображается немедленно, поскольку для вывода чисел на консоль выделен отдельный уникальный объект Thread:

Зачастую в многопоточной программе требуется, чтобы основной поток был последним потоком, завершающим ее выполнение. Формально программа продолжает выполняться до тех пор, пока не завершатся все ее приоритетные потоки. Поэтому требовать, чтобы основной поток завершал выполнение программы, совсем не обязательно. Тем не менее этого правила принято придерживаться в многопоточном программировании, поскольку оно явно определяет конечную точку программы.

Делегат ParametrizedThreadStart

Вспомните, что делегат ThreadStart может указывать только на методы, возвращающие void и не имеющие аргументов. Во многих случаях это подходит, но если нужно передать данные методу, выполняющемуся во вторичном потоке, то придется использовать тип делегата ParametrizedThreadStart.

Давайте рассмотрим пример:

Класс AutoResetEvent

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

Более безопасной, хотя также нежелательной альтернативой может быть вызов Thread.Sleep() на определенный период времени. Проблема в том, что нет желания ждать больше, чем необходимо.

Простой и безопасный к потокам способ заставить один поток ожидать завершения другого потока, предусматривает использование класса AutoResetEvent. В потоке, который должен ждать (таком как поток метода Main()), создадим экземпляр этого класса и передадим конструктору false, указав, что уведомления пока не было. В точке, где требуется ожидать, вызовем метод WaitOne(). Ниже приведен измененный класс Program, который делает все это, используя статическую переменную-член AutoResetEvent:

Ссылка на основную публикацию
Adblock
detector