Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Потоки_С#

.pdf
Скачиваний:
12
Добавлен:
14.05.2015
Размер:
201.91 Кб
Скачать

Работа с потоками в C#

C# поддерживает параллельное выполнение кода через многопоточность. Поток – это независимый путь исполнения, способный выполняться одновременно с другими потоками.

Программа на C# запускается как единственный поток, автоматически создаваемый CLR и операционной системой (“главный” поток), и становится многопоточной при помощи создания дополнительных потоков. Вот простой пример и его вывод:

Примечание: Все примеры предполагают, что импортируются следующие пространства имен (если этот момент специально не оговаривается):

Пример 1.

using System;

using System.Threading;

Вывод:

В главном потоке создается новый поток t, исполняющий метод, который непрерывно печатает символ ‘y’. Одновременно главный поток непрерывно печатает символ ‘x’.

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

1

Пример 2.

Консольный вывод:

Отдельный экземпляр переменной cycles создается в стеке каждого потока, так что выводится, как и ожидалось, десять знаков ‘?’.

Вместе с тем потоки разделяют данные, относящиеся к тому же экземпляру объекта, что и сами потоки:

Пример 3.

Так как оба потока вызывают метод Go() одного и того же экземпляра ThreadTest, они разделяют поле done. Результат – “Done”, напечатанное один раз вместо двух:

Для статических полей работает другой способ разделения данных между потоками. Вот тот же самый пример, но со статическим полем done:

2

Пример 4.

Оба примера демонстрируют также другое ключевое понятие – потоковую безопасность (или скорее её отсутствие). Фактически результат исполнения программы не определен: возможно (хотя и маловероятно), "Done" будет напечатано дважды. Однако если мы поменяем порядок вызовов в методе Go(), шансы увидеть “Done” напечатанным два раза повышаются радикально:

Вывод:

Проблема состоит в том, что один поток может выполнить оператор if, пока другой поток выполняет WriteLine, т.е. до того как done будет установлено в true.

Лекарство состоит в получении эксклюзивной блокировки на время чтения и записи разделяемых полей. C# обеспечивает это при помощи оператора lock:

3

Пример 5.

Когда два потока одновременно борются за блокировку (в нашем случае объекта locker), один поток переходит к ожиданию (блокируется), пока блокировка не освобождается. В данном случае это гарантирует, что только один поток может одновременно исполнять критическую секцию кода, и "Done" будет напечатано только один раз. Код, защищенный таким образом от неопределённости в плане многопоточного исполнения, называется потокобезопасным.

Временная приостановка (блокирование) – основной способ координации, или синхронизации действий потоков. Ожидание эксклюзивной блокировки – это одна из причин, по которым поток может блокироваться. Другая причина – если поток приостанавливается (Sleep) на заданный промежуток времени:

Также поток может ожидать завершения другого потока, вызывая его метод Join:

Будучи блокированным, поток не потребляет ресурсов CPU.

Именование потоков

Поток можно поименовать, используя свойство Name. Это предоставляет большое удобство при отладке: имена потоков можно вывести в Console.WriteLine и увидеть в окне Debug – Threads в Microsoft Visual Studio. Имя потоку может быть назначено в любой момент, но только один раз – при попытке изменить его будет сгенерировано исключение.

4

Главному потоку приложения также можно назначить имя – в следующем примере доступ к главному потоку осуществляется через статическое свойство CurrentThread класса Thread:

Пример 6.

Консольный вывод:

Основные и фоновые потоки

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

Примечание:

Изменение статуса потока с основного на фоновый не изменяет его приоритет или статус в планировщике потоков.

Статус потока переключается с основного на фоновый при помощи свойства IsBackground, как показано в следующем примере:

Пример 7.

Если программа вызывается без аргументов, рабочий поток выполняется по умолчанию как основной поток и ожидает на ReadLine, пока пользователь не нажмет Enter. Тем временем главный поток завершается, но приложение продолжает исполняться, так как рабочий поток еще жив.

5

Если же программу запустить с аргументами командной строки, рабочий поток получит статус фонового и программа завершится практически сразу после завершения главного потока, с уничтожением потока, ожидающего ввода пользователя с помощью метода ReadLine.

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

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

Передача параметра в поток

Нам необязательно создавать новый поток внутри метода Main. Давайте создадим класс который будет открывать новый поток в собственном конструкторе. Созданный нами класс по-прежнему будет заниматься счетом, но на этот раз он будет считать до указанного числа, за счет передачи потоку параметров.

Пример 8.

6

Разберем данный пример. Конструктор нашего класса myThread принимает 2 параметра: строку, в которой мы определяем имя потока, и номер до которого будет вестись счет в цикле. В конструкторе мы создаем поток, связанный с функцией func данного объекта. Далее мы присваиваем нашему потоку имя, используя поле Nameсозданного потока. И запускаем наш поток, передав функции Start аргумент.

Обратите внимание на то, что функция вызываемая потоком может принимать только 1 аргумент, и только типа object. В функции func цикл for досчитывает до числа, переданного ему как аргумент, и поток завершается. В функции Main мы создаем 3 пробных объекта, работа которых выведет на консоль примерно следующий текст.

Вывод:

Thread 1 выводит 0 Thread 1 выводит 1 Thread 2 выводит 0 Thread 2 выводит 1 Thread 2 выводит 2 Thread 2 завершился

<…>

Как работает многопоточность

Управление многопоточностью осуществляет планировщик потоков, эту функцию CLR обычно делегирует операционной системе. Планировщик потоков гарантирует, что активным потокам выделяется соответствующее время на выполнение, а потоки, ожидающие или блокированные, к примеру, на ожидании эксклюзивной блокировки, или пользовательского ввода – не потребляют времени CPU.

На однопроцессорных компьютерах планировщик потоков использует квантование времени – быстрое переключение между выполнением каждого из активных потоков. Это приводит к непредсказуемому поведению, как в самом первом примере, где каждая последовательность символов ‘X’ и ‘Y’ соответствует кванту времени, выделенному потоку. В Windows XP типичное значение кванта времени – десятки миллисекунд – выбрано как

7

намного большее, чем затраты CPU на переключение контекста между потоками (несколько микросекунд).

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

Говорят, что поток вытесняется, когда его выполнение приостанавливается из-за внешних факторов типа квантования времени. В большинстве случаев поток не может контролировать, когда и где он будет вытеснен.

Потоки vs. процессы

Все потоки одного приложения логически содержатся в пределах процесса – модуля операционной системы, в котором исполняется приложение.

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

Когда использовать потоки

Типовое приложение с многопоточностью выполняет длительные вычисления в фоновом режиме. Главный поток продолжает выполнение, в то время как рабочий поток выполняет фоновую задачу. В приложениях Windows Forms, когда главный поток занят длительными вычислениями, он не может обрабатывать сообщения клавиатуры и мыши, и приложение перестает откликаться. По этой причине следует запускать отнимающие много времени задачи в рабочем потоке, даже если главный поток в это время демонстрирует пользователю модальный диалог с надписью Работаю... Пожалуйста, ждите, так как программа не может перейти к следующей операции, пока не закончена текущая. Такое решение гарантирует, что приложение не будет помечено операционной системой как “Не отвечающее”, соблазняя пользователя с горя прикончить процесс. Опять же, в этом случае модальный диалог может предоставить кнопку “Отмена”, так как форма продолжает получать сообщения, пока задача выполняется в фоновом потоке. КлассBackgroundWorker наверняка пригодится вам при реализации такой модели.

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

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

C#-приложение можно сделать многопоточным двумя способами: либо явно создавая дополнительные потоки и управляя ими, либо используя возможности неявного создания

8

потоков .NET Framework – BackgroundWorker, пул потоков, потоковый таймер, Remotingсервер, Web-службы или приложение ASP.NET. В двух последних случаях альтернативы многопоточности не существует. Однопоточный web-сервер не просто плох, он попросту невозможен! К счастью, в случае серверов приложений, не хранящих состояние (stateless), многопоточность реализуется обычно довольно просто, сложности возможны разве что в синхронизации доступа к данным в статических переменных.

Когда потоки не нужны

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

Кроме того, чрезмерное использование многопоточности отнимает ресурсы и время CPU на создание потоков и переключение между потоками. В частности, когда используются операции чтения/записи на диск, более быстрым может оказаться последовательное выполнение задач в одном или двух потоках, чем одновременное их выполнение в нескольких потоках. Далее будет описана реализация очереди Поставщик/Потребитель, предоставляющей такую функциональность.

9