Восемь простых правил разработки многопоточных приложений. Многопоточные приложения для.NET Многопоточные программы с примеры

Глава №10.

Многопоточные приложения

Многозадачность в современных операционных системах воспринимается как нечто само собой разумеющееся [До появления Apple OS X на компьютерах Macintosh не было современных многозадачных операционных систем. Правильно спроектировать операционную систему с полноценной многозадачностью очень трудно, поэтому за основу OS X пришлось взять систему Unix. ]. Пользователь рассчитывает на то, что при одновременном запуске текстового редактора и почтового клиента эти программы не будут конфликтовать, а при приеме электронной почты редактор не перестанет работать. При одновременном запуске нескольких программ операционная система быстро переключается между программами, по очереди предоставляя им процессор (если, конечно, на компьютере не установлено несколько процессоров). В результате создается иллюзия одновременной работы нескольких программ, поскольку даже лучшая машинистка (и самое быстрое Интернет-соединение) не угонится за современным процессором.

Многопоточность (multithreading) в каком-то смысле можно рассматривать как следующий уровень многозадачности: вместо того, чтобы переключаться между разными программами, операционная система переключается между разными частями одной программы. Например, многопоточный почтовый клиент позволяет принимать новые сообщения электронной почты во время чтения или составления новых сообщений. В наше время многопоточность тоже воспринимается многими пользователями как должное.

В VB нормальной поддержки многопоточности не было никогда. Правда, в VB5 появилась одна из ее разновидностей - совместная потоковая модель (apartment threading). Как вы вскоре увидите, совместная модель предоставляет в распоряжение программиста часть преимуществ многопоточности, но не позволяет использовать все возможности в полной мере. Рано или поздно с учебной машины приходится пересаживаться на настоящую, и VB .NET стал первой версией VB с поддержкой свободной многопоточной модели.

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

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

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

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

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

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

Знакомство с многопоточностью

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

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

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

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

В программном потоке выполнятся процедура, а не объект.

Трудно сказать, что следует понимать под выражением «выполняется объект», но один из авторов часто ведет семинары по многопоточному программированию и этот вопрос задают чаще других. Возможно, кто-то полагает, что работа программного потока начинается с вызова метода New класса, после чего поток обрабатывает все сообщения, передаваемые соответствующему объекту. Такие представления абсолютно неверны. Один объект может содержать несколько потоков, выполняющих разные (а иногда даже одинаковые) методы, при этом сообщения объекта передаются и принимаются несколькими разными потоками (кстати, это одна из причин, затрудняющих многопоточное программирование: чтобы отладить программу, необходимо узнать, какой поток в данный момент выполняет ту или иную процедуру!).

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

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

Потоки могут завершаться не только естественно, но и аварийно. Обычно делать это не рекомендуется. За дополнительной информацией обращайтесь к разделу «Завершение и прерывание потоков».

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

Imports System.Threading

Импортирование пространства имен упрощает ввод программы и позволяет использовать технологию IntelliSense.

Непосредственная связь потоков с процедурами наводит на предположение о том, что в этой картине важное место занимают делегаты (см. главу 6). В частности, в пространство имен Threading входит делегат ThreadStart, обычно используемый при запуске программных потоков. Синтаксис использования этого делегата выглядит так:

Public Delegate Sub ThreadStart()

Код, вызываемый при помощи делегата ThreadStart, не должен иметь параметров и возвращаемого значения, поэтому потоки не могут создаваться для функций (которые возвращают значение) и для процедур с параметрами. Для передачи информации из потока тоже приходится искать альтернативные средства, поскольку выполняемые методы не возвращают значений и не могут использовать передачу по ссылке. Например, если процедура ThreadMethod находится в классе WilluseThread, то ThreadMethod может передавать информацию посредством изменения свойств экземпляров класса WillUseThread.

Домены приложений

Программные потоки.NET работают в так называемых доменах приложений, определяемых в документации как «изолированная среда, в которой выполняется приложение». Домен приложения можно рассматривать как облегченный вариант процессов Win32; один процесс Win32 может содержать несколько доменов приложений. Главное отличие между доменами приложений и процессами заключается в том, что процесс Win32 обладает самостоятельным адресным пространством (в документации домены приложений также сравниваются с логическими процессами, работающими внутри физического процесса). В.NET все управление памятью осуществляется исполнительной средой, поэтому в одном процессе Win32 могут работать несколько доменов приложений. Одним из преимуществ этой схемы является улучшение возможностей масштабирования (scaling) приложений. Средства для работы с доменами приложений находятся в классе AppDomain. Рекомендуем изучить документацию по этому классу. С его помощью можно получить информацию об окружении, в котором работает ваша программа. В частности, класс AppDomain применяется при выполнении рефлексии для системных классов.NET. Следующая программа выводит список загруженных сборок.

Imports System.Reflection

Module Modulel

Sub Main()

Dim theDomain As AppDomain

theDomain = AppDomain.CurrentDomain

Dim Assemblies()As

Assemblies = theDomain.GetAssemblies

Dim anAssemblyxAs

For Each anAssembly In Assemblies

Console.WriteLinetanAssembly.Full Name) Next

Console.ReadLine()

End Sub

End Module

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

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

Public Class WillUseThreads

Public Sub SubtractFromCounter()

Dim count As Integer

Do While True count -= 1

Console.WriteLlne("Am in another thread and counter ="

& count)

Loop

End Sub

End Class

Поскольку условие цикла Do остается истинным всегда, можно подумать, что ничто не помешает выполнению процедуры SubtractFromCounter. Тем не менее в многопоточном приложении это не всегда так.

В следующем фрагменте приведена процедура Sub Main, запускающая поток, и команда Imports:

Option Strict On Imports System.Threading Module Modulel

Sub Main()

1 Dim myTest As New WillUseThreads()

2 Dim bThreadStart As New ThreadStart(AddressOf _

myTest.SubtractFromCounter)

3 Dim bThread As New Thread(bThreadStart)

4 " bThread.Start()

Dim i As Integer

5 Do While True

Console.WriteLine("In main thread and count is " & i) i += 1

Loop

End Sub

End Module

Давайте последовательно разберем наиболее принципиальные моменты. Прежде всего процедура Sub Man n всегда работает в главном потоке (main thread). В програм-мах.NET всегда работают минимум два потока: главный и поток сборки мусора. В строке 1 создается новый экземпляр тестового класса. В строке 2 мы создаем делегат ThreadStart и передаем адрес процедуры SubtractFromCounter экземпляра тестового класса, созданного в строке 1 (эта процедура вызывается без параметров). Благо даря импортированию пространства имен Threading длинное имя можно не указывать. Объект нового потока создается в строке 3. Обратите внимание на передачу делегата ThreadStart при вызове конструктора класса Thread. Некоторые программисты предпочитают объединять эти две строки в одну логическую строку:

Dim bThread As New Thread(New ThreadStarttAddressOf _

myTest.SubtractFromCounter))

Наконец, строка 4 «запускает» поток, для чего вызывается метод Start экземпляра класса Thread, созданного для делегата ThreadStart. Вызывая этот метод, мы указываем операционной системе, что процедура Subtract должна работать в отдельном потоке.

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

На рис. 10.1 показан пример того, что может произойти после запуска программы и ее последующего прерывания клавишей Ctrl+Break. В нашем случае новый поток запустился лишь после того, как счетчик в главном потоке увеличился до 341!

Рис. 10.1. Простая многопоточная программно время работы

Если программа будет работать в течение большегошромежутка времени, результат будет выглядеть примерно так, как показано на рис. 10.2. Мы видим, что вы полнение запущенного потока приостанавливается и управление снова передается главному потоку. В данном случае имеет место проявление вытесняющей мно-гопоточности посредством квантования времени. Смысл этого устрашающего термина разъясняется ниже.

Рис. 10.2. Переключение между потоками в простой многопоточной программе

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

Поскольку в схемах квантования всех версий Windows, в которых работает.NET, каждо-му потоку выделяется минимальный квант времени, в программировании.NET проблемы с монопольным захватом процессора не столь серьезны. С другой стороны, если среда.NET когда-нибудь будет адаптирована для других систем, ситуация может измениться.

Если включить следующую строку в нашу программу перед вызовом Start, то даже потоки, обладающие минимальным приоритетом, получат некоторую долю процессорного времени:

bThread.Priority = ThreadPriority.Highest

Рис. 10.3. Поток с максимальным приоритетом обычно начинает работать быстрее

Рис. 10.4. Процессор предоставляется и потокам с более низким приоритетом

Команда назначает новому потоку максимальный приоритет и уменьшает приоритет главного потока. Из рис. 10.3 видно, что новый поток начинает работать быстрее, чем прежде, но, как показывает рис. 10.4, главный поток тоже получает управ ление (правда, очень ненадолго и лишь после продолжительной работы потока с вычитанием). При запуске программы на ваших компьютерах будут получены результаты, похожие на показанные на рис. 10.3 и 10.4, но из-за различий между нашими системами точного совпадения не будет.

В перечисляемый тип ThreadPrlority входят значения для пяти уровней приоритета:

ThreadPriority.Highest

ThreadPriority.AboveNormal

ThreadPrlority.Normal

ThreadPriority.BelowNormal

ThreadPriority.Lowest

Метод Join

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

thread2.Join()

приостанавливает текущий поток и ожидает завершения потока 2. Поток 1 переходит в заблокированное состояние.

Если присоединить поток 1 к потоку 2 методом Join, операционная система автоматически запустит поток 1 после завершения потока 2. Учтите, что процесс запуска является недетерминированным: нельзя точно сказать, через какой промежуток времени после завершения потока 2 заработает поток 1. Существует и другая версия Join, которая возвращает логическую величину:

thread2.Join(Integer)

Этот метод либо ожидает завершения потока 2, либо разблокирует поток 1 после истечения заданного интервала времени, вследствие чего планировщик операционной системы снова будет выделять потоку процессорное время. Метод возвращает True, если поток 2 завершается до истечения заданного интервала тайм-аута, и False в противном случае.

Не забывайте основное правило: независимо оттого, завершился ли поток 2 или про-изошел тайм-аут, вы не можете управлять моментом активизации потока 1.

Имена потоков, CurrentThread и ThreadState

Свойство Thread.CurrentThread возвращает ссылку на объект потока, выполняемого в настоящий момент.

Хотя для отладки многопоточных приложений в VB .NET существует замечательное окно потоков, о котором рассказано далее, нас очень часто выручала команда

MsgBox(Thread.CurrentThread.Name)

Нередко выяснялось, что код выполняется совсем не в том потоке, в котором ему полагалось выполняться.

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

Окно потоков

Окно потоков (Threads window) Visual Studio .NET оказывает неоценимую помощь в отладке многопоточных программ. Оно активизируется командой подменю Debug > Windows в режиме прерывания. Допустим, вы назначили имя потоку bThread следующей командой:

bThread.Name = "Subtracting thread"

Примерный вид окна потоков после прерывания программы комбинацией клавиш Ctrl+Break (или другим способом) показан на рис. 10.5.

Рис. 10.5. Окно потоков

Стрелкой в первом столбце помечается активный поток, возвращаемый свойством Thread.CurrentThread. Столбец ID содержит числовые идентификаторы потоков. В следующем столбце перечислены имена потоков (если они были присвоены). Столбец Location указывает выполняемую процедуру (например, процедура WriteLine класса Console на рис. 10.5). Остальные столбцы содержат информацию о приоритете и приостановленных потоках (см. следующий раздел).

Окно потоков (а не операционная система!) позволяет управлять потоками вашей программы при помощи контекстных меню. Например, вы можете остановить текущий поток, для чего следует щелкнуть в соответствующей строке правой кнопкой мыши и выбрать команду Freeze (позже работу остановленного потока можно возобновить). Остановка потоков часто используемая при отладке, чтобы неправильно работающий поток не мешал работе приложения. Кроме того, окно потоков позволяет активизировать другой (не остановленный) поток; для этого следует щелкнуть правой кнопкой мыши в нужной строке и выбрать в контекстном меню команду Switch To Thread (или просто сделать двойной щелчок на строке потока). Как будет показано далee, это очень удобно при диагностике потенциальных взаимных блокировок (deadlocks).

Приостановка потока

Временно неиспользуемые потоки можно перевести в пассивное состояние методом Slеер. Пассивный поток также считается заблокированным. Разумеется, с переводом потока в пассивное состояние на долю остальных потоков достанется больше ресурсов процессора. Стандартный синтаксис метода Slеер выглядит следующим образом: Thread.Sleep(интервал_в_миллисекундах)

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

Другая версия Sleep заставляет текущий поток уступить оставшуюся часть выделенного процессорного времени:

Thread.Sleep(0)

Следующий вариант переводит текущий поток в пассивное состояние на неограниченное время (активизация происходит только при вызове Interrupt):

Thread.Slеер(Timeout.Infinite)

Поскольку пассивные потоки (даже при неограниченном времени ожидания) могут прерываться методом Interrupt, что приводит к инициированию исключения ThreadlnterruptExcepti on, вызов Slеер всегда заключается в блок Try-Catch, как в следующем фрагменте:

Try

Thread.Sleep(200)

" Пассивное состояние потока было прервано

Catch e As Exception

"Остальные исключения

End Try

Каждая программа.NET работает в программном потоке, поэтому метод Sleep также используется для приостановки работы программ (если пространство имен Threadipg не импортируется программой, приходится использовать полное имя Threading.Thread. Sleep).

Завершение или прерывание программных потоков

Поток автоматически завершается при выходе из метода, указанного при создании делегата ThreadStart, но иногда требуется завершить метод (следовательно, и поток) при возникновении определенных факторов. В таких случаях в потоках обычно проверяется условная переменная, в зависимости от состояния которой принимается решение об аварийном выходе из потока. Как правило, для этого в процедуру включается цикл Do-While:

Sub ThreadedMethod()

" В программе необходимо предусмотреть средства для опроса

" условной переменной.

" Например, условную переменную можно оформить в виде свойства

Do While conditionVariable = False And MoreWorkToDo

" Основной код

Loop End Sub

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

Если проверка условной переменной должна происходить в строго определенном месте, воспользуйтесь командой If-Then в сочетании с Exit Sub внутри бесконечного цикла.

Доступ к условной переменной необходимо синхронизировать, чтобы воздействие со стороны других потоков не помешало ее нормальному использованию. Этой важной теме посвящен раздел «Решение проблемы: синхронизация».

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

Метод Interrupt может вызываться только для потоков, находящихся в состоянии Wait, Sleep или Join. Если вызвать Interrupt для потока, находящегося в одном из перечисленных состояний, то через некоторое время поток снова начнет работать, а исполнительная среда инициирует в потоке исключение ThreadlnterruptedExcepti on. Это происходит даже в том случае, если поток был переведен в пассивное состояние на неопределенный срок вызовом Thread.Sleepdimeout. Infinite). Мы говорим «через некоторое время», поскольку планирование потоков имеет недетерминированную природу. Исключение ThreadlnterruptedExcepti on перехватывается секцией Catch, содержащей код выхода из состояния ожидания. Тем не менее секция Catch вовсе не обязана завершать поток по вызову Interrupt - поток обрабатывает исключение по своему усмотрению.

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

Приостановка и уничтожение потоков

Пространство имен Threading содержит и другие методы, прерывающие нормальное функционирование потоков:

  • Suspend;
  • Abort.

Трудно сказать, зачем в.NET была включена поддержка этих методов - при вызове Suspend и Abort программа, скорее всего, начнет работать нестабильно. Ни один из методов не позволяет нормально провести деинициализацию потока. Кроме того, при вызове Suspend или Abort невозможно предсказать, в каком состоянии поток оставит объекты после приостановки или аварийного завершения.

В результате вызова Abort инициируется исключение ThreadAbortException. Чтобы вы поняли, почему это странное исключение не следует обрабатывать в программах, мы приводим отрывок из документации.NET SDK:

«...При уничтожении потока вызовом Abort исполнительная среда инициирует исключение ThreadAbortException. Это особая разновидность исключений, которая не может перехватываться программой. При инициировании этого исключения перед тем, как уничтожить поток, исполнительная среда выполняет все блоки Finally. Поскольку в блоках Finally могут выполняться любые действия, вызовите Join, чтобы убедиться в уничтожении потока».

Мораль: Abort и Suspend использовать не рекомендуется (а если без Suspend все же не обойтись, возобновите приостановленный поток методом Resume). Безопасно завершить поток можно только путем опроса синхронизируемой условной переменной или вызовом метода Interrupt, о котором говорилось выше.

Фоновые потоки (демоны)

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

Если в приложении остались только фоновые потоки, приложение автоматически завершается.

Более серьезный пример: извлечение данных из кода HTML

Мы рекомендуем использовать потоки лишь в том случае, когда функциональность программы четко делится на несколько операций. Хорошим примером является программа извлечения данных из кода HTML из главы 9. Наш класс выполняет две операции: выборку данных с сайта Amazon и их обработку. Перед нами идеальный пример ситуации, в которой многопоточное программирование действительно уместно. Мы создаем классы для нескольких разных книг и затем анализируем данные в разных потоках. Создание нового потока для каждой книги повышает эффективность программы, поскольку во время приема данных одним потоком (что может потребовать ожидания на сервере Amazon) другой поток будет занят обработкой уже полученных данных.

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

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

Public Sub FindRank()

m_Rank = ScrapeAmazon()

Console.WriteLine("the rank of " & m_Name & "Is " & GetRank)

End Sub

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

Dim theBook(3.1) As String theBook(0.0) = "1893115992"

theBook(0.l) = "Programming VB .NET" " И т.д.

Четыре потока создаются в том же цикле, в котором создаются объекты AmazonRanker:

For i= 0 То 3

Try

theRanker = New AmazonRanker(theBook(i.0). theBookd.1))

aThreadStart = New ThreadStar(AddressOf theRanker.FindRan()

aThread = New Thread(aThreadStart)

aThread.Name = theBook(i.l)

aThread.Start() Catch e As Exception

Console.WriteLine(e.Message)

End Try

Next

Ниже приведен полный текст программы:

Option Strict On Imports System.IO Imports System.Net

Imports System.Threading

Module Modulel

Sub Main()

Dim theBook(3.1) As String

theBook(0.0) = "1893115992"

theBook(0.l) = "Programming VB .NET"

theBook(l.0) = "1893115291"

theBook(l.l) = "Database Programming VB .NET"

theBook(2,0) = "1893115623"

theBook(2.1) = "Programmer "s Introduction to C#."

theBook(3.0) = "1893115593"

theBook(3.1) = "Gland the .Net Platform "

Dim i As Integer

Dim theRanker As =AmazonRanker

Dim aThreadStart As Threading.ThreadStart

Dim aThread As Threading.Thread

For i = 0 To 3

Try

theRanker = New AmazonRankerttheBook(i.0). theBook(i.1))

aThreadStart = New ThreadStart(AddressOf theRanker. FindRank)

aThread = New Thread(aThreadStart)

aThread.Name= theBook(i.l)

aThread.Start()

Catch e As Exception

Console.WriteLlnete.Message)

End Try Next

Console.ReadLine()

End Sub

End Module

Public Class AmazonRanker

Private m_URL As String

Private m_Rank As Integer

Private m_Name As String

Public Sub New(ByVal ISBN As String. ByVal theName As String)

m_URL = "http://www.amazon.com/exec/obidos/ASIN/" & ISBN

m_Name = theName End Sub

Public Sub FindRank() m_Rank = ScrapeAmazon()

Console.Writeline("the rank of " & m_Name & "is "

& GetRank) End Sub

Public Readonly Property GetRank() As String Get

If m_Rank <> 0 Then

Return CStr(m_Rank) Else

" Проблемы

End If

End Get

End Property

Public Readonly Property GetName() As String Get

Return m_Name

End Get

End Property

Private Function ScrapeAmazon() As Integer Try

Dim theURL As New Uri(m_URL)

Dim theRequest As WebRequest

theRequest =WebRequest.Create(theURL)

Dim theResponse As WebResponse

theResponse = theRequest.GetResponse

Dim aReader As New StreamReader(theResponse.GetResponseStream())

Dim theData As String

theData = aReader.ReadToEnd

Return Analyze(theData)

Catch E As Exception

Console.WriteLine(E.Message)

Console.WriteLine(E.StackTrace)

Console. ReadLine()

End Try End Function

Private Function Analyze(ByVal theData As String) As Integer

Dim Location As.Integer Location = theData.IndexOf("Amazon.com

Sales Rank:") _

+ "Amazon.com Sales Rank:".Length

Dim temp As String

Do Until theData.Substring(Location.l) = "<" temp = temp

&theData.Substring(Location.l) Location += 1 Loop

Return Clnt(temp)

End Function

End Class

Многопоточные операции часто используются в.NET и пространствах имен ввода-вы-вода, поэтому в библиотеке.NET Framework для них предусмотрены специальные асинхронные методы. Дополнительная информация о применении асинхронных методов при написании многопоточных программ приведена в описании методов BeginGetResponse и EndGetResponse класса HTTPWebRequest

Главная опасность (общие данные)

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

Вашему вниманию предлагается небольшая программа, которая демонстрирует возникающие проблемы, не углубляясь в излишние подробности. В этой программе моделируется дом, в каждой комнате которого установлен термостат. Если температура на 5 и более градусов по Фаренгейту (около 2,77 градусов по Цельсию) меньше положенной, мы приказываем системе отопления повысить температуру на 5 градусов; в противном случае температура повышается только на 1 градус. Если текущая температура больше либо равна заданной, изменение не производится. Регулировка температуры в каждой комнате осуществляется отдельным потоком с 200-миллисекундной задержкой. Основная работа выполняется следующим фрагментом:

If mHouse.HouseTemp < mHouse.MAX_TEMP = 5 Then Try

Thread.Sleep(200)

Catch tie As ThreadlnterruptedException

" Пассивное ожидание было прервано

Catch e As Exception

" Другие исключения End Try

mHouse.HouseTemp +- 5 " И т.д.

Ниже приведен полный исходный текст программы. Результат показан на рис. 10.6: температура в доме достигла 105 градусов по Фаренгейту (40,5 градуса по Цельсию)!

1 Option Strict On

2 Imports System.Threading

3 Module Modulel

4 Sub Main()

5 Dim myHouse As New House(l0)

6 Console. ReadLine()

7 End Sub

8 End Module

9 Public Class House

10 Public Const MAX_TEMP As Integer = 75

11 Private mCurTemp As Integer = 55

12 Private mRooms() As Room

13 Public Sub New(ByVal numOfRooms As Integer)

14 ReDim mRooms(numOfRooms = 1)

15 Dim i As Integer

16 Dim aThreadStart As Threading.ThreadStart

17 Dim aThread As Thread

18 For i = 0 To numOfRooms -1

19 Try

20 mRooms(i)=NewRoom(Me, mCurTemp,CStr(i) &"throom")

21 aThreadStart - New ThreadStart(AddressOf _

mRooms(i).CheckTempInRoom)

22 aThread =New Thread(aThreadStart)

23 aThread.Start()

24 Catch E As Exception

25 Console.WriteLine(E.StackTrace)

26 End Try

27 Next

28 End Sub

29 Public Property HouseTemp()As Integer

30 . Get

31 Return mCurTemp

32 End Get

33 Set(ByVal Value As Integer)

34 mCurTemp = Value 35 End Set

36 End Property

37 End Class

38 Public Class Room

39 Private mCurTemp As Integer

40 Private mName As String

41 Private mHouse As House

42 Public Sub New(ByVal theHouse As House,

ByVal temp As Integer, ByVal roomName As String)

43 mHouse = theHouse

44 mCurTemp = temp

45 mName = roomName

46 End Sub

47 Public Sub CheckTempInRoom()

48 ChangeTemperature()

49 End Sub

50 Private Sub ChangeTemperature()

51 Try

52 If mHouse.HouseTemp < mHouse.MAX_TEMP - 5 Then

53 Thread.Sleep(200)

54 mHouse.HouseTemp +- 5

55 Console.WriteLine("Am in " & Me.mName & _

56 ".Current temperature is "&mHouse.HouseTemp)

57 . Elself mHouse.HouseTemp < mHouse.MAX_TEMP Then

58 Thread.Sleep(200)

59 mHouse.HouseTemp += 1

60 Console.WriteLine("Am in " & Me.mName & _

61 ".Current temperature is " & mHouse.HouseTemp)

62 Else

63 Console.WriteLine("Am in " & Me.mName & _

64 ".Current temperature is " & mHouse.HouseTemp)

65 " Ничего не делать, температура нормальная

66 End If

67 Catch tae As ThreadlnterruptedException

68 " Пассивное ожидание было прервано

69 Catch e As Exception

70 " Другие исключения

71 End Try

72 End Sub

73 End Class

Рис. 10.6. Проблемы многопоточности

В процедуре Sub Main (строки 4-7) создается «дом» с десятью «комнатами». Класс House устанавливает максимальную температуру 75 градусов по Фаренгейту (около 24 градусов по Цельсию). В строках 13-28 определяется довольно сложный конструктор дома. Ключевыми для понимания программы являются строки 18-27. Строка 20 создает очередной объект комнаты, при этом конструктору передается ссылка на объект дома, чтобы объект комнаты при необходимости мог к нему обратиться. Строки 21-23 запускают десять потоков для регулировки температуры в каждой комнате. Класс Room определяется в строках 38-73. Ссылка на объект House coxpa няется в переменной mHouse в конструкторе класса Room (строка 43). Код проверки и регулировки температуры (строки 50-66) выглядит просто и естественно, но как вы вскоре убедитесь, это впечатление обманчиво! Обратите внимание на то, что этот код заключен в блок Try-Catch, поскольку в программе используется метод Sleep.

Вряд ли кто-нибудь согласится жить при температуре в 105 градусов по Фаренгейту (40,5 24 градусов по Цельсию). Что же произошло? Проблема связана со следующей строкой:

If mHouse.HouseTemp < mHouse.MAX_TEMP - 5 Then

А происходит следующее: сначала температуру проверяет поток 1. Он видит, что температура слишком низка, и поднимает ее на 5 градусов. К сожалению, перед повышением температуры поток 1 прерывается и управление передаётся поток 2. Поток 2 проверяет ту же самую переменную, которая еще не была изменена потоком 1. Таким образом, поток 2 тоже готовится поднять температуру на 5 градусов, но сделать этого не успевает и тоже переходит в состояние ожидания. Процесс продолжается до тех пор, пока поток 1 не активизируется и не перейдет к следующей команде - повышению температуры на 5 градусов. Повышение повторяется при активизации всех 10 потоков, и жильцам дома придется плохо.

Решение проблемы: синхронизация

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

If mHouse.HouseTemp < mHouse.MAX_TEMP - 5 Then...

полностью отрабатываются активным потоком до того, как он будет прерван. Это свойство называется атомарностыд - блок кода должен выполняться каждым потоком без прерывания, как атомарная единица. Группа команд, объединенных в атомарный блок, не может быть прервана планировщиком потоков до ее завершения. В любом многопоточном языке программирования существуют свои способы обеспечения атомарности. В VB .NET проще всего воспользоваться командой SyncLock, при вызове которой передается объектная переменная. Внесите в процедуру ChangeTemperature из предыдущего примера небольшие изменения, и программа заработает нормально:

Private Sub ChangeTemperature() SyncLock (mHouse)

Try

If mHouse.HouseTemp < mHouse.MAXJTEMP -5 Then

Thread.Sleep(200)

mHouse.HouseTemp += 5

Console.WriteLine("Am in " & Me.mName & _

".Current temperature is " & mHouse.HouseTemp)

Elself

mHouse.HouseTemp < mHouse. MAX_TEMP Then

Thread.Sleep(200) mHouse.HouseTemp += 1

Console.WriteLine("Am in " & Me.mName &_ ".Current temperature is " & mHouse.HomeTemp) Else

Console.WriteLineC"Am in " & Me.mName & _ ".Current temperature is " & mHouse.HouseTemp)

" Ничего не делать, температура нормальная

End If Catch tie As ThreadlnterruptedException

" Пассивное ожидание было прервано Catch e As Exception

" Другие исключения

End Try

End SyncLock

End Sub

Код блока SyncLock выполняется атомарно. Доступ к нему со стороны всех остальных потоков будет закрыт, пока первый поток не снимет блокировку командой End SyncLock. Если поток в синхронизируемом блоке переходит в состояние пассивного ожидания, блокировка сохраняется вплоть до прерывания или возобновления работы потока.

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

Базовые классы коллекций небезопасны в многопоточных приложениях, но в.NET Framework входят поточно-безопасные версии большинства классов коллекций. В этих классах код потенциально опасных методов заключается в блоки SyncLock. Поточно-безопасные версии классов коллекций следует использовать в многопоточных программах везде, где возникает угроза целостности данных.

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

Public Class ConditionVariable

Private Shared locker As Object= New Object()

Private Shared mOK As Boolean Shared

Property TheConditionVariable()As Boolean

Get

Return mOK

End Get

Set(ByVal Value As Boolean) SyncLock (locker)

mOK= Value

End SyncLock

End Set

End Property

End Class

Команда SyncLock и класс Monitor

Использование команды SyncLock связано с некоторыми тонкостями, не проявившимися в приведенных выше простых примерах. Так, очень важную роль играет выбор объекта синхронизации. Попробуйте запустить предыдущую программу с командой SyncLock(Me) вместо SyncLock(mHouse). Температура снова поднимается выше пороговой величины!

Помните, что команда SyncLock производит синхронизацию по объекту, переданному в качестве параметра, а не по фрагменту кода. Параметр SyncLock играет роль двери для обращения к синхронизируемому фрагменту из других потоков. Команда SyncLock(Me) фактически открывает несколько разных «дверей», а ведь именно этого вы и пытались избежать при помощи синхронизации. Мораль:

Для защиты общих данных в многопоточном приложении команда SyncLock должна синхронизироваться по одному объекту.

Поскольку синхронизация связана с конкретным объектом, в некоторых ситуациях возможна непреднамеренная блокировка других фрагментов. Допустим, у вас имеются два синхронизированных метода first и second, причем оба метода синхронизируются по объекту bigLock. Когда поток 1 входит в метод first и захватывает bigLock, ни один поток не сможет войти в метод second, потому что доступ к нему уже ограничен потоком 1!

Функциональность команды SyncLock можно рассматривать как подмножество функциональности класса Monitor. Класс Monitor обладает расширенными возможностями настройки, и с его помощью можно решать нетривиальные задачи синхронизации. Команда SyncLock является приближенным аналогом методов Enter и Exi t класса Moni tor:

Try

Monitor.Enter(theObject) Finally

Monitor.Exit(theObject)

End Try

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

Взаимная блокировка

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

Рассмотрим ситуацию взаимной блокировки (deadlock) в простейшем виде. Представьте себе двух программистов за обеденным столом. К сожалению, на двоих у них только один нож и одна вилка. Если предположить, что для еды нужны и нож и вилка, возможны две ситуации:

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

В многопоточной программе подобная ситуация называется взаимной блокировкой. Два метода синхронизируются по разным объектам. Поток А захватывает объект 1 и входит во фрагмент программы, защищенный этим объектом. К сожалению, для работы ему необходим доступ к коду, защищенному другим блоком Sync Lock с другим объектом синхронизации. Но прежде, чем он успевает войти во фрагмент, синхронизируемый другим объектом, в него входит поток В и захватывает этот объект. Теперь поток А не может войти во второй фрагмент, поток В не может войти в первый фрагмент, и оба потока обречены на бесконечное ожидание. Ни один поток не может продолжить работу, поскольку необходимый для этого объект так и не будет освобожден.

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

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

1 Option Strict On

2 Imports System.Threading

3 Module Modulel

4 Sub Main()

5 Dim Tom As New Programmer("Tom")

6 Dim Bob As New Programmer("Bob")

7 Dim aThreadStart As New ThreadStart(AddressOf Tom.Eat)

8 Dim aThread As New Thread(aThreadStart)

9 aThread.Name= "Tom"

10 Dim bThreadStart As New ThreadStarttAddressOf Bob.Eat)

11 Dim bThread As New Thread(bThreadStart)

12 bThread.Name = "Bob"

13 aThread.Start()

14 bThread.Start()

15 End Sub

16 End Module

17 Public Class Fork

18 Private Shared mForkAvaiTable As Boolean = True

19 Private Shared mOwner As String = "Nobody"

20 Private Readonly Property OwnsUtensil() As String

21 Get

22 Return mOwner

23 End Get

24 End Property

25 Public Sub GrabForktByVal a As Programmer)

26 Console.Writel_ine(Thread.CurrentThread.Name &_

"trying to grab the fork.")

27 Console.WriteLine(Me.OwnsUtensil & "has the fork.") . .

28 Monitor.Enter(Me) "SyncLock (aFork)"

29 If mForkAvailable Then

30 a.HasFork = True

31 mOwner = a.MyName

32 mForkAvailable = False

33 Console.WriteLine(a.MyName&"just got the fork.waiting")

34 Try

Thread.Sleep(100) Catch e As Exception Console.WriteLine (e.StackTrace)

End Try

35 End If

36 Monitor.Exit(Me)

End SyncLock

37 End Sub

38 End Class

39 Public Class Knife

40 Private Shared mKnifeAvailable As Boolean = True

41 Private Shared mOwner As String ="Nobody"

42 Private Readonly Property OwnsUtensi1() As String

43 Get

44 Return mOwner

45 End Get

46 End Property

47 Public Sub GrabKnifetByVal a As Programmer)

48 Console.WriteLine(Thread.CurrentThread.Name & _

"trying to grab the knife.")

49 Console.WriteLine(Me.OwnsUtensil & "has the knife.")

50 Monitor.Enter(Me) "SyncLock (aKnife)"

51 If mKnifeAvailable Then

52 mKnifeAvailable = False

53 a.HasKnife = True

54 mOwner = a.MyName

55 Console.WriteLine(a.MyName&"just got the knife.waiting")

56 Try

Thread.Sleep(100)

Catch e As Exception

Console.WriteLine (e.StackTrace)

End Try

57 End If

58 Monitor.Exit(Me)

59 End Sub

60 End Class

61 Public Class Programmer

62 Private mName As String

63 Private Shared mFork As Fork

64 Private Shared mKnife As Knife

65 Private mHasKnife As Boolean

66 Private mHasFork As Boolean

67 Shared Sub New()

68 mFork = New Fork()

69 mKnife = New Knife()

70 End Sub

71 Public Sub New(ByVal theName As String)

72 mName = theName

73 End Sub

74 Public Readonly Property MyName() As String

75 Get

76 Return mName

77 End Get

78 End Property

79 Public Property HasKnife() As Boolean

80 Get

81 Return mHasKnife

82 End Get

83 Set(ByVal Value As Boolean)

84 mHasKnife = Value

85 End Set

86 End Property

87 Public Property HasFork() As Boolean

88 Get

89 Return mHasFork

90 End Get

91 Set(ByVal Value As Boolean)

92 mHasFork = Value

93 End Set

94 End Property

95 Public Sub Eat()

96 Do Until Me.HasKnife And Me.HasFork

97 Console.Writeline(Thread.CurrentThread.Name&"is in the thread.")

98 If Rnd() < 0.5 Then

99 mFork.GrabFork(Me)

100 Else

101 mKnife.GrabKnife(Me)

102 End If

103 Loop

104 MsgBox(Me.MyName & "can eat!")

105 mKnife = New Knife()

106 mFork= New Fork()

107 End Sub

108 End Class

Основная процедура Main (строки 4-16) создает два экземпляра класса Programmer и затем запускает два потока для выполнения критического метода Eat класса Programmer (строки 95-108), описанного ниже. Процедура Main задает имена потоков и занускает их; вероятно, все происходящее понятно и без комментариев.

Интереснее выглядит код класса Fork (строки 17-38) (аналогичный класс Knife определяется в строках 39-60). В строках 18 и 19 задаются значения общих полей, по которым можно узнать, доступна ли в данный момент вилка, и если нет - кто ею пользуется. ReadOnly-свойство OwnUtensi1 (строки 20-24) предназначено для простейшей передачи информации. Центральное место в классе Fork занимает метод «захвата вилки» GrabFork, определяемый в строках 25-27.

  1. Строки 26 и 27 просто выводят на консоль отладочную информацию. В основном коде метода (строки 28-36) доступ к вилке синхронизируется по объектной пе ременной Me. Поскольку в нашей программе используется только одна вилка, синхронизация по Me гарантирует, что два потока не смогут одновременно захватить ее. Команда Slee"p (в блоке, начинающемся в строке 34) имитирует задержку между захватом вилки/ножа и началом еды. Учтите, что команда Sleep не снимает блокировку с объектов и лишь ускоряет возникновение взаимной блокировки!
    Однако наибольший интерес представляет код класса Programmer (строки 61-108). В строках 67-70 определяется общий конструктор, что гарантирует наличие в программе только одной вилки и ножа. Код свойств (строки 74-94) прост и не требует комментариев. Самое главное происходит в методе Eat, выполняемом двумя отдельными потоками. Процесс продолжается в цикле до тех пор, пока какой-либо поток не захватит вилку вместе с ножом. В строках 98-102 объект случайным образом захватывает вилку/нож, используя вызов Rnd, - именно это и порождает взаимную блокировку. Происходит следующее:
    Поток, выполняющий метод Eat объекта Тот, активизируется и входит в цикл. Он захватывает нож и переходит в состояние ожидания.
  2. Поток, выполняющий метод Eat объекта Bob, активизируется и входит в цикл. Он не может захватить нож, но захватывает вилку и переходит в состояние ожидания.
  3. Поток, выполняющий метод Eat объекта Тот, активизируется и входит в цикл. Он пытается захватить вилку, однако вилка уже захвачена объектом Bob; поток переходит в состояние ожидания.
  4. Поток, выполняющий метод Eat объекта Bob, активизируется и входит в цикл. Он пытается захватить нож, однако нож уже захвачен объектом Тот; поток переходит в состояние ожидания.

Все это продолжается до бесконечности - перед нами типичная ситуация взаимной блокировки (попробуйте запустить программу, и вы убедитесь в том, что поесть так никому и не удается).
О возникновении взаимной блокировки можно узнать и в окне потоков. Запустите программу и прервите ее клавишами Ctrl+Break. Включите в окно просмотра переменную Me и откройте окно потоков. Результат выглядит примерно так, как показано на рис. 10.7. Из рисунка видно, что поток Bob захватил нож, но вилки у него нет. Щелкните правой кнопкой мыши в окне потоков на строке Тот и выберите в контекстном меню команду Switch to Thread. Окно просмотра показывает, что у потока Тот имеется вилка, но нет ножа. Конечно, это не является стопроцентным доказательством, но подобное поведение по крайней мере заставляет заподозрить неладное.
Если вариант с синхронизацией по одному объекту (как в программе с повышением -температуры в доме) невозможен, для предотвращения взаимных блокировок можно пронумеровать объекты синхронизации и всегда захватывать их в постоянном порядке. Продолжим аналогию с обедающими программистами: если поток всегда сначала берет нож, а потом вилку, проблем с взаимной блокировкой не будет. Первый поток, захвативший нож, сможет нормально поесть. В переводе на язык программных потоков это означает, что захват объекта 2 возможен лишь при условии предварительного захвата объекта 1.

Рис. 10.7. Анализ взаимной блокировки в окне потоков

Следовательно, если убрать вызов Rnd в строке 98 и заменить его фрагментом

mFork.GrabFork(Me)

mKnife.GrabKnife(Me)

взаимная блокировка исчезает!

Совместная работа с данными по мере их создания

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

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

  • Поток 1 (потребитель) активизируется, входите синхронизированный метод, ищет данные, не находит их и переходит в состояние ожидания. Предвари телъно он должен снять блокировку, чтобы не мешать работе потока- поставщика.
  • Поток 2 (поставщик) входит в синхронизированный метод, освобожденный потоком 1, создает данные для потока 1 и каким-то образом оповещает поток 1 о наличии данных. Затем он снимает блокировку, чтобы поток 1 смог обработать новые данные.

Не пытайтесь решить эту проблему постоянной активизацией потока 1 с проверкой состояния условной переменной, значение которой>устанавливается потоком 2. Такое решение серьезно повлияет на быстродействие вашей программы, поскольку в большинстве случаев поток 1 будет активизироваться без всяких причин; а поток 2 будет переходить в ожидание так часто, что у него не останется времени на создание данных.

Связи «поставщик/потребитель» встречаются очень часто, поэтому в библиотеках классов многопоточного программирования для таких ситуаций создаются специальные примитивы. В.NET эти примитивы называются Wait и Pulse-PulseAl 1 и являются частью класса Monitor. Рисунок 10.8 поясняет ситуацию, которую мы собираемся запрограммировать. В программе организуются три очереди потоков: очередь ожидания, очередь блокировки и очередь выполнения. Планировщик потоков не выделяет процессорное время потокам, находящимся в очереди ожидания. Чтобы потоку выделялось время, он должен переместиться в очередь выполнения. В результате работа приложения организуется гораздо эффективнее, чем при обычном опросе условной переменной.

На псевдокоде идиома потребителя данных формулируется так:

" Вход в синхронизированный блок следующего вида

While нет данных

Перейти в очередь ожидания

Loop

Если данные есть, обработать их.

Покинуть синхронизированный блок

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

На псевдокоде идиома поставщика данных выглядит так:

" Вход в синхронизированный блок вида

While данные НЕ нужны

Перейти в очередь ожидания

Else Произвести данные

После появления готовых данных вызвать Pulse-PulseAll.

чтобы переместить один или несколько потоков из очереди блокировки в очередь выполнения. Покинуть синхронизированный блок (и вернуться в очередь выполнения)

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

1 Option Strict On

2 Imports System.Threading

3 Module Modulel

4 Sub Main()

5 Dim theFamily As New Family()

6 theFamily.StartltsLife()

7 End Sub

8 End fjodule

9

10 Public Class Family

11 Private mMoney As Integer

12 Private mWeek As Integer = 1

13 Public Sub StartltsLife()

14 Dim aThreadStart As New ThreadStarUAddressOf Me.Produce)

15 Dim bThreadStart As New ThreadStarUAddressOf Me.Consume)

16 Dim aThread As New Thread(aThreadStart)

17 Dim bThread As New Thread(bThreadStart)

18 aThread.Name = "Produce"

19 aThread.Start()

20 bThread.Name = "Consume"

21 bThread. Start()

22 End Sub

23 Public Property TheWeek() As Integer

24 Get

25 Return mweek

26 End Get

27 Set(ByVal Value As Integer)

28 mweek - Value

29 End Set

30 End Property

31 Public Property OurMoney() As Integer

32 Get

33 Return mMoney

34 End Get

35 Set(ByVal Value As Integer)

36 mMoney =Value

37 End Set

38 End Property

39 Public Sub Produce()

40 Thread.Sleep(500)

41 Do

42 Monitor.Enter(Me)

43 Do While Me.OurMoney > 0

44 Monitor.Wait(Me)

45 Loop

46 Me.OurMoney =1000

47 Monitor.PulseAll(Me)

48 Monitor.Exit(Me)

49 Loop

50 End Sub

51 Public Sub Consume()

52 MsgBox("Am in consume thread")

53 Do

54 Monitor.Enter(Me)

55 Do While Me.OurMoney = 0

56 Monitor.Wait(Me)

57 Loop

58 Console.WriteLine("Dear parent I just spent all your " & _

money in week " & TheWeek)

59 TheWeek += 1

60 If TheWeek = 21 *52 Then System.Environment.Exit(0)

61 Me.OurMoney =0

62 Monitor.PulseAll(Me)

63 Monitor.Exit(Me)

64 Loop

65 End Sub

66 End Class

Метод StartltsLife (строки 13-22) осуществляет подготовку к запуску потоков Produce и Consume. Самое главное происходит в потоках Produce (строки 39-50) и Consume (строки 51-65). Процедура Sub Produce проверяет наличие денег, и если деньги есть, переходит в очередь ожидания. В противном случае родитель генерирует деньги (строка 46) и оповещает объекты в очереди ожидания об изменении ситуации. Учтите, что вызов Pulse-Pulse All вступает в силу лишь при снятии блокировки командой Monitor.Exit. И наоборот, процедура Sub Consume проверяет наличие денег, и если денег нет - оповещает об этом ожидающего родителя. Строка 60 просто завершает программу по прошествии 21 условного года; вызов System. Environment.Exit(0) является.NET-аналогом команды End (команда End тоже поддерживается, но в отличие от System. Environment. Exit она не позволяет вернуть код завершения операционной системе).

Потоки, переведенные в очередь ожидания, должны быть освобождены другими час-тями вашей программы. Именно по этой причине мы предпочитаем использовать PulseAll вместо Pulse. Поскольку заранее неизвестно, какой именно поток будет активизирован при вызове Pulse 1 , при относительно небольшом количестве потоков в очереди с таким же успехом можно вызвать PulseAll.

Многопоточность в графических программах

Наше обсуждение многопоточности в приложениях с графическим интерфейсом начнется с примера, поясняющего, для чего нужна многопоточность в графических приложениях. Создайте форму с двумя кнопками Start (btnStart) и Cancel (btnCancel), как показано на рис. 10.9. При нажатии кнопки Start создается класс, который содержит случайную строку из 10 миллионов символов и метод для подсчета вхождений буквы «Е» в этой длинной строке. Обратите внимание на применение класса StringBuilder, повышающего эффективность создания длинных строк.

Шаг 1

Поток 1 замечает, что данных для него нет. Он вызывает Wait, снимает блокировку и переходит в очередь ожидания



Шаг 2

При снятии блокировки поток 2 или поток 3 выходит из очереди блокировки и входит в синхронизированный блок, устанавливая блокировку

ШагЗ

Допустим, поток 3 входит в синхронизированный блок, создает данные и вызывает Pulse-Pulse All.

Сразу же после его выхода из блока и снятия блокировки поток 1 перемещается в очередь выполнения. Если поток 3 вызывает Pluse, в очередь выполнения переходит только один поток, при вызове Pluse All в очередь выполнения переходят все потоки.



Рис. 10.8. Проблема «поставщик/потребитель»

Рис. 10.9. Многопоточность в простом приложении с графическим интерфейсом

Imports System.Text

Public Class RandomCharacters

Private m_Data As StringBuilder

Private mjength, m_count As Integer

Public Sub New(ByVal n As Integer)

m_Length = n -1

m_Data = New StringBuilder(m_length) MakeString()

End Sub

Private Sub MakeString()

Dim i As Integer

Dim myRnd As New Random()

For i = 0 To m_length

" Сгенерировать случайное число от 65 до 90,

" преобразовать его в прописную букву

" и присоединить к объекту StringBuilder

m_Data.Append(Chr(myRnd.Next(65.90)))

Next

End Sub

Public Sub StartCount()

GetEes()

End Sub

Private Sub GetEes()

Dim i As Integer

For i = 0 To m_length

If m_Data.Chars(i) = CChar("E") Then

m_count += 1

End If Next

m_CountDone = True

End Sub

Public Readonly

Property GetCount() As Integer Get

If Not (m_CountDone) Then

Return m_count

End If

End Get End Property

Public Readonly

Property IsDone()As Boolean Get

Return

m_CountDone

End Get

End Property

End Class

С двумя кнопками на форме связывается весьма простой код. В процедуре btn-Start_Click создается экземпляр приведенного выше класса RandomCharacters, инкапсулирующего строку с 10 миллионами символов:

Private Sub btnStart_Click(ByVal sender As System.Object.

ByVal e As System.EventArgs) Handles btnSTart.Click

Dim RC As New RandomCharacters(10000000)

RC.StartCount()

MsgBox("The number of es is " & RC.GetCount)

End Sub

Кнопка Cancel выводит окно сообщения:

Private Sub btnCancel_Click(ByVal sender As System.Object._

ByVal e As System.EventArgs)Handles btnCancel.Click

MsgBox("Count Interrupted!")

End Sub

При запуске программы и нажатии кнопки Start выясняется, что кнопка Cancel не реагирует на действия пользователя, поскольку непрерывный цикл не позволяет кнопке обработать полученное событие. В современных программах подобное недопустимо!

Возможны два решения. Первый вариант, хорошо знакомый по предыдущим версиям VB, обходится без многопоточности: в цикл включается вызов DoEvents. В.NET эта команда выглядит так:

Application.DoEvents()

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

Следующий шаг: кнопка Show Count

Допустим, вы решили проявить творческую фантазию и придать форме вид, показанный на рис. 10.9. Обратите внимание: кнопка Show Count пока недоступна.

Рис. 10.10. Форма с заблокированной кнопкой

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

Public Class RandomCharacters

Private m_0ata As StringBuilder

Private m_CountDone As Boolean

Private mjength. m_count As Integer

Private m_Button As Windows.Forms.Button

Public Sub New(ByVa1 n As Integer,_

ByVal b As Windows.Forms.Button)

m_length = n - 1

m_Data = New StringBuilder(mJength)

m_Button = b MakeString()

End Sub

Private Sub MakeString()

Dim I As Integer

Dim myRnd As New Random()

For I = 0 To m_length

m_Data.Append(Chr(myRnd.Next(65. 90)))

Next

End Sub

Public Sub StartCount()

GetEes()

End Sub

Private Sub GetEes()

Dim I As Integer

For I = 0 To mjength

If m_Data.Chars(I) = CChar("E") Then

m_count += 1

End If Next

m_CountDone =True

m_Button.Enabled=True

End Sub

Public Readonly

Property GetCount()As Integer

Get

If Not (m_CountDone) Then

Throw New Exception("Count not yet done") Else

Return m_count

End If

End Get

End Property

Public Readonly Property IsDone() As Boolean

Get

Return m_CountDone

End Get

End Property

End Class

Вполне вероятно, что в некоторых случаях этот код будет работать. Тем не менее:

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

Если вы нарушите эти правила, мы гарантируем, что в ваших многопоточных графических программах будут возникать тонкие, неуловимые ошибки.

Организовать взаимодействие объектов с применением событий тоже не удастся. 06-работник события выполняется в том же потоке, в котором произошел вызов RaiseEvent поэтому события вам не помогут.

И все же здравый смысл подсказывает, что в графических приложениях должны существовать средства модификации элементов из другого потока. В.NET Framework существует поточно-безопасный способ вызова методов приложений GUI из другого потока. Для этой цели используется особый тип делегатов Method Invoker из пространства имен System.Windows. Forms. В следующем фрагменте приведен новый вариант метода GetEes (измененные строки выделены жирным шрифтом):

Private Sub GetEes()

Dim I As Integer

For I = 0 To m_length

If m_Data.Chars(I) = CChar("E")Then

m_count += 1

End If Next

m_CountDone = True Try

Dim mylnvoker As New Methodlnvoker(AddressOf UpDateButton)

myInvoker.Invoke() Catch e As ThreadlnterruptedException

"Неудача

End Try

End Sub

Public Sub UpDateButton()

m_Button.Enabled =True

End Sub

Межпоточные обращения к кнопке осуществляются не напрямую, а через Method Invoker. .NET Framework гарантирует, что этот вариант безопасен по отношению к потокам.

Почему при многопоточном программировании возникает столько проблем?

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

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

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

Но и это не все: современные компиляторы оптимизируют быстродействие программ, а оборудование компьютера может вмешиваться в процесс управления памятью. Как следствие, компилятор или оборудование может без вашего ведома изменить порядок команд, указанный в исходном тексте программы [Многие компиляторы оптимизируют циклические операции копирования массивов вида for i=0 to n:b(i)=a(i):ncxt. Компилятор (или даже специализированное устройство управления памятью) может просто создать массив, а потом заполнить его одной операцией копирования вместо многократного копирования отдельных элементов! ].

Надеемся, эти пояснения помогут вам лучше понять, почему многопоточное программирование порождает столько проблем, - или по крайней мере меньше удивляться при виде странного поведения ваших многопоточных программ!

Андрей Колесов

Приступая к рассмотрению принципов создания многопоточных приложений для среды Microsoft .NET Framework, сразу оговоримся: хотя все примеры приведены на Visual Basic .NET, методика создания таких программ в целом одинакова для всех языков программирования, поддерживающих.NET, в том числе для C#. VB выбран для демонстрации технологии создания многопоточных приложений в первую очередь потому, что предыдущие версии этого инструмента такой возможности не предоставляли.

Осторожно: Visual Basic .NET тоже может делать ЭТО!

Как известно, Visual Basic (до версии 6.0 включительно) никогда ранее не позволял создавать многопоточные программные компоненты (EXE, ActiveX DLL и OCX). Тут нужно вспомнить, что архитектура COM включает три разные потоковые модели: однопоточную (Single Thread), совместную (Single Threaded Apartment, STA) и свободную (Multi-Threaded Apartment). VB 6.0 позволяет создавать программы первых двух типов. Вариант STA предусматривает псевдомногопоточный режим - несколько потоков действительно работают параллельно, но при этом программный код каждого из них защищен от доступа к нему извне (в частности, потоки не могут использовать общие ресурсы).

Visual Basic .NET теперь может реализовать свободную многопоточность в ее настоящем (native) варианте. Точнее сказать, в.NET такой режим поддерживается на уровне общих библиотек классов Class Library и среды исполнения Common Language Runtime. В результате VB.NET наравне с другими языками программирования.NET получил доступ к этим возможностям.

В свое время сообщество VB-разработчиков, выражая недовольство многими будущими новшествами этого языка, с большим одобрением отнеслось к известию о том, что с помощью новой версии инструмента можно будет создавать многопоточные программы (см. "В ожидании Visual Studio .NET", "BYTE/Россия" № 1/2001). Однако многие эксперты высказывали более сдержанные оценки по поводу этого новшества. Вот, например, мнение Дана Эпплмана (Dan Appleman), известного разработчика и автора многочисленных книг для VB-программистов: "Многопоточность в VB.NET страшит меня больше, чем все остальные новшества, причем, как и во многих новых технологиях.NET, это объясняется скорее человеческими, нежели технологическими факторами... Я боюсь многопоточности в VB.NET, потому что VB-программисты обычно не обладают опытом проектирования и отладки многопоточных приложений" .

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

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

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

Параллельная обработка в VB6

Конечно, организовать псевдопараллельную обработку данных можно было и с помощью VB6, но возможности эти были весьма ограниченными. Например, мне несколько лет назад понадобилось написать процедуру, которая приостанавливает выполнение программы на указанное число секунд (соответствующий оператор SLEEP в готовом виде присутствовал в Microsoft Basic/DOS). Ее нетрудно реализовать самостоятельно в виде следующей простой подпрограммы:

В ее работоспособности можно легко убедиться, например, с помощью такого кода обработки щелчка кнопки на форме:

Чтобы решить эту проблему в VB6, внутри цикла Do...Loop процедуры SleepVB нужно снять комментарий с обращения к функции DoEvents, которая передает управление операционной системе и возвращает число открытых форм в данном VB-приложении. Но обратите внимание, что вывод окна с сообщением "Еще один привет!", в свою очередь, блокирует выполнение всего приложения, в том числе и процедуры SleepVB.

Используя глобальные переменные в качестве флагов, можно обеспечить также аварийное завершение запущенной процедуры SleepVB. Она, в свою очередь, представляет собой простейший пример вычислительного процесса, полностью занимающего ресурсы процессора. Но если вы будете совершать какие-то полезные вычисления (а не крутиться в пустом цикле), то нужно иметь в виду, что обращение к функции DoEvent занимает довольно много времени, поэтому это нужно делать через достаточно большие интервалы времени.

Чтобы увидеть ограниченность поддержки параллельных вычислений в VB6, замените обращение к функции DoEvents на вывод метки:

Label1.Caption = Timer

В этом случае не только не будет срабатывать кнопка Command2, но даже в течение 5 с не будет изменяться содержание метки.

Для проведения еще одного эксперимента добавьте вызов ожидания в код для Command2 (это можно сделать, так как процедура SleepVB реентерабельна):

Private Sub Command2_Click() Call SleepVB(5) MsgBox "Еще один привет!" End Sub

Далее запустите приложение и щелкните Command1, а спустя 2-3 с - Command2. Первым появится сообщение "Еще один привет"!, хотя соответствующий процесс был запущен позднее. Причина этого в том, что функция DoEvents проверяет только события визуальных элементов, но не наличие других вычислительных потоков. Более того, VB-приложение фактически работает в одном потоке, поэтому управление вернулось в событийную процедуру, которая была запущена последней.

Управление потоками в.NET

Построение многопоточных.NET-приложений основывается на использовании группы базовых классов.NET Framework, описываемых пространством имен System.Threading. При этом ключевая роль принадлежит классу Thread, с помощью которого выполняются практически все операции по управлению потоками. С этого места все сказанное о работе с потоками относится ко всем средствам программирования в.NET, в том числе к C#.

Для первого знакомства с созданием параллельных потоков создадим Windows-приложение с формой, на которой разместим кнопки ButtonStart и ButtonAbort и напишем следующий код:

Сразу же хотелось бы обратить внимание на три момента. Во-первых, ключевые слова Imports используются для обращения к сокращенным именам классов, описанных здесь пространством имен. Я специально привел еще один вариант применения Imports для описания сокращенного эквивалента длинного названия пространства имен (VB = Microsoft.VisualBasic), который можно применить к тексту программы. В этом случае сразу видно, к какому пространству имен относится объект Timer.

Во-вторых, я использовал логические скобки #Region, чтобы наглядно отделить код, написанный мной, от кода, формируемого дизайнером форм автоматически (последний здесь не приводится).

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

Запустите приложение и щелкните кнопку ButtonStart. Запустился процесс ожидания в цикле заданного интервала времени, причем в данном случае (в отличие от примера с VB6) - в независимом потоке. В этом легко убедиться - все визуальные элементы формы являются доступными. Например, нажав кнопку ButtonAbort, можно аварийно завершить процесс с помощью метода Abort (но закрытие формы с помощью системной кнопки Close не прервет выполнение процедуры!). Для наглядности динамики процесса вы можете разместить на форме метку, а в цикл ожидания процедуры SleepVBNET добавить вывод текущего времени:

Label1.Text = _ "Текущее время = " & VB.TimeOfDay

Выполнение процедуры SleepVBNET (которая в данном случае уже представляет собой метод нового объекта) будет продолжаться, даже если вы добавите в код ButtonStart вывод окна сообщения о начале вычислений после запуска потока (рис. 1).

Более сложный вариант - поток в виде класса

Для проведения дальнейших экспериментов с потоками создадим новое VB-приложение типа Console, состоящее из обычного модуля кода с процедурой Main (которая начинает выполняться при запуске приложения) и модуля класса WorkerThreadClass:

Запустим созданное приложение. Появится консольное окно, в котором будет видна бегущая строка символов, демонстрирующая модель запущенного вычислительного процесса (WorkerThread). Потом появится окно сообщения, выданного вызывающим процессом (Main), и в завершение мы увидим картинку, изображенную на рис. 2 (если вас не устраивает скорость выполнения моделируемого процесса, то уберите или добавьте какие-нибудь арифметические операции с переменной "а" в процедуре WorkerThread).

Обратите внимание: окно сообщения "Запущен первый поток" было выдано на экран с заметной задержкой, после старта процесса WorkerThread (в случае с формой, описанном в предыдущем пункте, такое сообщение появилось бы почти мгновенно после нажатия кнопки ButtonStart). Скорее всего, это происходит потому, что при работе с формой событийные процедуры имеют более высокий приоритет по сравнению с запускаемым процессом. В случае же консольного приложения все процедуры имеют одинаковый приоритет. Вопрос приоритетов мы обсудим позднее, а пока установим для вызывающего потока (Main) самый высокий приоритет:

Thread.CurrentThread.Priority = _ ThreadPriority.Highest Thread1.Start()

Теперь окно появляется почти сразу. Как видим, создавать экземпляры объекта Thread можно двумя способами. Сначала мы применяли первый из них - создали новый объект (поток) Thread1 и работали с ним. Второй вариант - получить объект Thread для выполняемого в данный момент потока с помощью статического метода CurrentThread. Именно таким образом процедура Main сама для себя установила более высокий приоритет, но могла она это сделать и для любого другого потока, например:

Thread1.Priority = ThreadPriority.Lowest Thread1.Start()

Чтобы показать возможности управления запущенным процессом, добавим в конце процедуры Main такие строчки кода:

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

Сначала в консольном окне начнется "Процесс 1", и появится сообщение "Первый поток запущен". "Процесс 1" выполняется, а вы быстренько нажмите кнопку ОК в окне сообщения.

Далее - "Процесс 1" продолжается, но через две секунды появляется сообщение "Поток приостановлен". "Процесс 1" замер. Нажмите кнопку "ОК" в окне сообщения: "Процесс 1" продолжил свое выполнение и успешно завершил его.

В этом фрагменте мы использовали метод Sleep для приостановки текущего процесса. Заметьте: Sleep является статическим методом и может применяться только к текущему процессу, но не к какому-то экземпляру объекта Thread. Синтаксис языка позволяет написать Thread1.Sleep или Thread.Sleep, но все равно в этом случае используется объект CurrentThread.

Метод Sleep может также использовать аргумент 0. В этом случае текущий поток освободит неиспользованный остаток кванта выделенного для него времени.

Еще один интересный вариант использования Sleep - со значением Timeout.Infinite. В этом случае поток будет приостановлен на неопределенный срок, пока это состояние не будет прервано другим потоком с помощью метода Thread.Interrupt.

Чтобы приостановить внешний поток из другого потока без остановки последнего, нужно использовать вызов метода Thread.Suspend. Тогда продолжить его выполнение можно будет методом Thread.Resume, что мы и сделали в приведенном выше коде.

Немного о синхронизации потоков

Синхронизация потоков - это одна из главных задач при написании многопоточных приложений, и в пространстве System.Threading имеется большой набор средств для ее решения. Но сейчас мы познакомимся только с методом Thread.Join, который позволяет отлеживать окончание выполнение потока. Чтобы увидеть, как он работает, замените последние строки процедуры Main на такой код:

Управление приоритетами процессов

Распределение квантов времени процессора между потоками выполняется с помощью приоритетов, которые задаются в виде свойства Thread.Priority. Для потоков, создаваемых в период выполнения, можно устанавливать пять значений: Highest, AboveNormal, Normal (используется по умолчанию), BelowNormal и Lowest. Чтобы посмотреть, как влияют приоритеты на скорость выполнения потоков, напишем такой код для процедуры Main:

Sub Main() " описание первого процесса Dim Thread1 As Thread Dim oWorker1 As New WorkerThreadClass() Thread1 = New Thread(AddressOf _ oWorker1.WorkerThread) " Thread1.Priority = _ " ThreadPriority.BelowNormal " передаем исходные данные: oWorker1.Start = 1 oWorker1.Finish = 10 oWorker1.ThreadName = "Отсчет 1" oWorker1.SymThread = "." " описание второго процесса Dim Thread2 As Thread Dim oWorker2 As New WorkerThreadClass() Thread2 = New Thread(AddressOf _ oWorker2.WorkerThread) " передаем исходные данные: oWorker2.Start = 11 oWorker2.Finish = 20 oWorker2.ThreadName = "Отсчет 2" oWorker2.SymThread = "*" " " запускаем наперегонки Thread.CurrentThread.Priority = _ ThreadPriority.Highest Thread1.Start() Thread2.Start() " Ждем завершения процессов Thread1.Join() Thread2.Join() MsgBox("Оба процесса завершились") End Sub

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

Теперь перед запуском первого потока установим для него приоритет на один уровень ниже:

Thread1.Priority = _ ThreadPriority.BelowNormal

Картина резко поменялась: второй поток практически полностью отнял все время у первого (рис. 4).

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

Заключение

Мы лишь затронули основы разработки многопоточных.NET-приложений. Один из наиболее сложных и на практике актуальных вопросов - это синхронизация потоков. Кроме применения описанного в этой статье объекта Thread (у него есть много методов и свойств, которые мы не рассматривали здесь), очень важную роль в управлении потоками играют классы Monitor и Mutex, а также операторы lock (C#) и SyncLock (VB.NET).

Более подробное описание этой технологии приведено в отдельных главах книг и , из которых мне хотелось бы привести несколько цитат (с которыми я полностью согласен) в качестве очень краткого подведения итогов по теме "Многопоточность в.NET".

"Если вы новичок, для вас может быть неожиданностью обнаружить, что издержки, связанные с созданием и диспетчеризацией потоков, могут привести к тому, что однопоточное приложение работает быстрее... Поэтому всегда старайтесь протестировать оба прототипа программы - однопоточный и многопоточный" .

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

"Не следует рассматривать применение многопоточности как подход по умолчанию" .

"Я спросил аудиторию, состоящую из опытных VB-программистов, хотя ли они получить свободную многопоточность будущей версии VB. Практически все подняли руки. Затем я спросил, кто знает, на что он идет при этом. На этот раз руки подняли всего несколько человек, и на их лицах были понимающие улыбки" .

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

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

Литература:

  1. Дан Эпплман. Переход на VB.NET: стратегии, концепции, код/Пер. с англ. - СПб.: "Питер", 2002, - 464 с.: ил.
  2. Том Арчер. Основы C#. Новейшие технологии/Пер. с англ. - М.: Издательско-торговый дом "Русская Редакция", 2001. - 448 с.: ил.

Многозадачность и многопоточность

Начнем с такого простого утверждения: 32-разрядные операционные системы Windows поддерживают многозадачные (многопроцессные) и многопоточные режимы обработки данных. Можно обсуждать, насколько хорошо они это делают, но это уже другой вопрос.

Многозадачность - это режим работы, когда компьютер может выполнять несколько задач одновременно, параллельно. Понятно, что если компьютер имеет один процессор, то речь идет о псевдопараллельности, когда ОС по некоторым правилам может выполнять быстрое переключение между различными задачами. Задача - это программа или часть программы (приложения), выполняющая некоторое логическое действие и являющаяся единицей, для которой ОС выделяет ресурсы. Несколько в упрощенном виде можно считать, что в Windows задачей является каждый программный компонент, реализованный в виде отдельного исполняемого модуля (EXE, DLL). Для Windows понятие "задача" имеет тот же смысл, что и "процесс", что, в частности, означает выполнение программного кода строго в отведенном для него адресном пространстве.

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

Вытесняющая многозадачность - режим, когда сама ОС отвечает за выдачу каждому потоку причитающегося ему кванта времени (time-slice), по истечении которого она (при наличии запросов от других задач) автоматически прерывает этот поток и принимает решение, что запускать далее. Раньше этот режим так и назывался - "с разделением времени".

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

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

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

Тут также можно вспомнить, что при эксплуатации мощных вычислительных систем коллективного пользования, родоначальником которых стало в конце 60-х годов семейство IBM System/360, одной из наиболее актуальных задач был выбор оптимального варианта управления многозадачностью - в том числе в динамическом режиме с учетом различных параметров. В принципе управление многозадачным режимом - это функция операционной системы. Но эффективность реализации того или иного варианта непосредственно связана с особенностями архитектуры компьютера в целом, и особенно процессора. Например, та же высокопроизводительная IBM System/360 отлично работала в системах коллективного пользования в сфере бизнес-задач, но при этом она была совершенно не приспособлена для решения задач класса "реального масштаба времени". В этой области тогда явно лидировали существенно более дешевые и простые мини-компьютеры типа DEC PDP 11/20.

В более ранних постах было рассказано про многопоточность в Windows при помощи CreateThread и прочего WinAPI , а также многопоточность в Linux и других *nix системах при помощи pthreads . Если вы пишите на C++11 или более поздних версиях, то вам доступны std::thread и другие многопоточные примитивы, появившиеся в этом стандарте языка. Далее будет показано, как с ними работать. В отличие от WinAPI и pthreads, код, написанный на std::thread, является кроссплатформенным.

Примечание: Приведенный код был проверен на GCC 7.1 и Clang 4.0 под Arch Linux , GCC 5.4 и Clang 3.8 под Ubuntu 16.04 LTS, GCC 5.4 и Clang 3.8 под FreeBSD 11, а также Visual Studio Community 2017 под Windows 10. CMake до версии 3.8 не умеет говорить компилятору использовать стандарт C++17, указанный в свойствах проекта. Как установить CMake 3.8 в Ubuntu 16.04 . Чтобы код компилировался при помощи Clang, в *nix системах должен быть установлен пакет libc++. Для Arch Linux пакет доступен на AUR . В Ubuntu есть пакет libc++-dev, но вы можете столкнуться с , из-за которого код так просто собираться не будет. Воркэраунд описан на StackOverflow . Во FreeBSD для компиляции проекта нужно установить пакет cmake-modules.

Мьютексы

Ниже приведен простейший пример использования трэдов и мьютексов:

#include
#include
#include
#include

Std:: mutex mtx;
static int counter = 0 ;


for (;; ) {
{
std:: lock_guard < std:: mutex > lock(mtx) ;

break ;
int ctr_val = ++ counter;
std:: cout << "Thread " << tnum << ": counter = " <<
ctr_val << std:: endl ;
}

}
}

int main() {
std:: vector < std:: thread > threads;
for (int i = 0 ; i < 10 ; i++ ) {


}

// can"t use const auto& here since .join() is not marked const

thr.join () ;
}

Std:: cout << "Done!" << std:: endl ;
return 0 ;
}

Обратите внимание на оборачивание std::mutex в std::lock_guard в соответствии c идиомой RAII . Такой подход гарантирует, что мьютекс будет отпущен при выходе из скоупа в любом случае, в том числе при возникновении исключений. Для захвата сразу нескольких мьютексов с целью предотвращения дэдлоков существует класс std::scoped_lock . Однако он появился только в C++17 и потому может работать не везде. Для более ранних версий C++ есть аналогичный по функционалу шаблон std::lock , правда для корректного освобождения локов по RAII он требует написания дополнительного кода.

RWLock

Нередко возникает ситуация, в которой доступ к объекту чаще происходит на чтение, чем на запись. В этом случае вместо обычного мьютекса эффективнее использовать read-write lock, он же RWLock. RWLock может быть захвачен сразу несколькими потоками на чтение, или только одним потоком на запись. RWLock’у в C++ соответствуют классы std::shared_mutex и std::shared_timed_mutex:

#include
#include
#include
#include

// std::shared_mutex mtx; // will not work with GCC 5.4
std:: shared_timed_mutex mtx;

static int counter = 0 ;
static const int MAX_COUNTER_VAL = 100 ;

void thread_proc(int tnum) {
for (;; ) {
{
// see also std::shared_lock
std:: unique_lock < std:: shared_timed_mutex > lock(mtx) ;
if (counter == MAX_COUNTER_VAL)
break ;
int ctr_val = ++ counter;
std:: cout << "Thread " << tnum << ": counter = " <<
ctr_val << std:: endl ;
}
std:: this_thread :: sleep_for (std:: chrono :: milliseconds (10 ) ) ;
}
}

int main() {
std:: vector < std:: thread > threads;
for (int i = 0 ; i < 10 ; i++ ) {
std:: thread thr(thread_proc, i) ;
threads.emplace_back (std:: move (thr) ) ;
}

for (auto & thr : threads) {
thr.join () ;
}

Std:: cout << "Done!" << std:: endl ;
return 0 ;
}

По аналогии с std::lock_guard для захвата RWLock’а используются классы std::unique_lock и std::shared_lock, в зависимости от того, как мы хотим захватить лок. Класс std::shared_timed_mutex появился в C++14 и работает на всех* современных платформах (не скажу за мобильные устройства, игровые консоли, и так далее). В отличие от std::shared_mutex , он имеет методы try_lock_for, try_lock_unti и другие, которые пытаются захватить мьютекс в течение заданного времени. Я сильно подозреваю, что std::shared_mutex должен быть дешевле std::shared_timed_mutex. Однако std::shared_mutex появился только в C++17, а значит поддерживается не везде. В частности, все еще широко используемый GCC 5.4 про него не знает.

Thread Local Storage

Иногда бывает нужно создать переменную, вроде глобальной, но которую видит только один поток. Другие потоки тоже видят переменную, но у них она имеет свое локальное значение. Для этого придумали Thread Local Storage, или TLS (не имеет ничего общего с Transport Layer Security !). Помимо прочего, TLS может быть использован для существенного ускорения генерации псевдослучайных чисел. Пример использования TLS на C++:

#include
#include
#include
#include

Std:: mutex io_mtx;
thread_local int counter = 0 ;
static const int MAX_COUNTER_VAL = 10 ;

void thread_proc(int tnum) {
for (;; ) {
counter++ ;
if (counter == MAX_COUNTER_VAL)
break ;
{
std:: lock_guard < std:: mutex > lock(io_mtx) ;
std:: cout << "Thread " << tnum << ": counter = " <<
counter << std:: endl ;
}
std:: this_thread :: sleep_for (std:: chrono :: milliseconds (10 ) ) ;
}
}

int main() {
std:: vector < std:: thread > threads;
for (int i = 0 ; i < 10 ; i++ ) {
std:: thread thr(thread_proc, i) ;
threads.emplace_back (std:: move (thr) ) ;
}

for (auto & thr : threads) {
thr.join () ;
}

Std:: cout << "Done!" << std:: endl ;
return 0 ;
}

Мьютекс здесь используется исключительно для синхронизации вывода в консоль. Для доступа к thread_local переменным никакая синхронизация не требуется.

Атомарные переменные

Атомарные переменные часто используются для выполнения простых операций без использования мьютексов. Например, вам нужно инкрементировать счетчик из нескольких потоков. Вместо того, чтобы оборачивать int в std::mutex, эффективнее воспользоваться std::atomic_int. Также C++ предлагает типы std::atomic_char, std::atomic_bool и многие другие . Еще на атомарных переменных реализуют lock-free алгоритмы и структуры данных. Стоит отметить, что они весьма сложны в разработке и отладке, и не на всех системах работают быстрее аналогичных алгоритмов и структур данных с локами.

Пример кода:

#include
#include
#include
#include
#include

static std:: atomic_int atomic_counter(0 ) ;
static const int MAX_COUNTER_VAL = 100 ;

Std:: mutex io_mtx;

void thread_proc(int tnum) {
for (;; ) {
{
int ctr_val = ++ atomic_counter;
if (ctr_val >= MAX_COUNTER_VAL)
break ;

{
std:: lock_guard < std:: mutex > lock(io_mtx) ;
std:: cout << "Thread " << tnum << ": counter = " <<
ctr_val << std:: endl ;
}
}
std:: this_thread :: sleep_for (std:: chrono :: milliseconds (10 ) ) ;
}
}

int main() {
std:: vector < std:: thread > threads;

int nthreads = std:: thread :: hardware_concurrency () ;
if (nthreads == 0 ) nthreads = 2 ;

for (int i = 0 ; i < nthreads; i++ ) {
std:: thread thr(thread_proc, i) ;
threads.emplace_back (std:: move (thr) ) ;
}

for (auto & thr : threads) {
thr.join () ;
}

Std:: cout << "Done!" << std:: endl ;
return 0 ;
}

Обратите внимание на использование процедуры hardware_concurrency. Она возвращает оценку количества трэдов, которое в текущей системе может выполняться параллельно. Например, на машине с четырехядерным процессором, поддерживающим hyper threading, процедура возвращает число 8. Также процедура может возвращать ноль, если сделать оценку не удалось или процедура попросту не реализована.

Кое-какую информацию о работе атомарных переменных на уровне ассемблера можно найти в заметке Шпаргалка по основным инструкциям ассемблера x86/x64 .

Заключение

Насколько я вижу, все это действительно неплохо работает. То есть, при написании кроссплатформенных приложений на C++ про WinAPI и pthreads можно благополучно забыть. В чистом C начиная с C11 также существуют кроссплатформенные трэды . Но они все еще не поддерживаются Visual Studio (я проверил) , и вряд ли когда-либо будут поддерживаться. Не секрет, что Microsoft не видит интереса в развитии поддержки языка C в своем компиляторе, предпочитая концентрироваться на C++.

За кадром осталось еще немало примитивов: std::condition_variable(_any), std::(shared_)future, std::promise, std::sync и другие. Для ознакомления с ними я рекомендую сайт cppreference.com . Также может иметь смысл прочитать книгу C++ Concurrency in Action . Но должен предупредить, что она уже не новая, содержит многовато воды, и в сущности пересказывает десяток статей с cppreference.com.

Полная версия исходников к этой заметке, как обычно, лежит на GitHub . А как вы сейчас пишите многопоточные приложения на C++?

Пример посторения простого многопоточного приложения.

Рожден о причине большого числа вопросов о построении многопоточных приложений в Delphi.

Цель данного примера - продемонстрировать как правильно строить многопоточное приложение, с выносом длительной работы в отдельный поток. И как в таком приложении обеспечить взаимодействие основного потока с рабчим для передачи данных из формы (визуальных компонентов) в поток и обратно.

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

Итак пример. Для удобства поместил и код, и прикрепил архив с кодом модуля и формы

unit ExThreadForm;

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;

// константы используемые при передаче данных из потока в форму с помощью
// отсылки оконных сообщений
const
WM_USER_SendMessageMetod = WM_USER+10;
WM_USER_PostMessageMetod = WM_USER+11;

type
// описание класса потока, потомка от tThread
tMyThread = class(tThread)
private
SyncDataN:Integer;
SyncDataS:String;
procedure SyncMetod1;
protected
procedure Execute; override;
public
Param1:String;
Param2:Integer;
Param3:Boolean;
Stopped:Boolean;
LastRandom:Integer;
IterationNo:Integer;
ResultList:tStringList;

Constructor Create (aParam1:String);
destructor Destroy; override;
end;

// описание класса использующей поток формы
TForm1 = class(TForm)
Label1: TLabel;
Memo1: TMemo;
btnStart: TButton;
btnStop: TButton;
Edit1: TEdit;
Edit2: TEdit;
CheckBox1: TCheckBox;
Label2: TLabel;
Label3: TLabel;
Label4: TLabel;
procedure btnStartClick(Sender: TObject);
procedure btnStopClick(Sender: TObject);
private
{ Private declarations }
MyThread:tMyThread;
procedure EventMyThreadOnTerminate (Sender:tObject);
procedure EventOnSendMessageMetod (var Msg: TMessage);message WM_USER_SendMessageMetod;
procedure EventOnPostMessageMetod (var Msg: TMessage); message WM_USER_PostMessageMetod;

Public
{ Public declarations }
end;

var
Form1: TForm1;

{
Stopped - демонстрирует передачу данных от формы к потоку.
Дополнительной синхронизации не требует, поскольку является простым
однословным типом, и пишется только одним потоком.
}

procedure TForm1.btnStartClick(Sender: TObject);
begin
Randomize(); // обеспечение случайнсти в последовательности по Random() - к потоком отношения не имеет

// Создание экземпляра объекта потока, с передачей ему входного параметра
{
ВНИМАНИЕ!
Конструктор потока написан таким образом что поток создается
приостановленным, поскольку это позволяет:
1. Контролировать момент его запуска. Это почти всегда удобнее, т.к.
позволяет еще до запуска настроить поток, передать ему входные
параметры, и т.п.
2. Т.к. ссылка на созданный объект будет сохранена в поле формы, то
после самоуничтожения потока (см.ниже) которое при запущенном потоке
может произойти в любой момент, эта ссылка станет недействительной.
}
MyThread:= tMyThread.Create(Form1.Edit1.Text);

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

// Назначение обработчика завершения потока в котором будем принимать
// результаты работы потока, и "затирать" ссылку на него
MyThread.OnTerminate:= EventMyThreadOnTerminate;

// Поскольку результаты будем забирать в OnTerminate, т.е. до самоуничтожения
// потока то снимем с себя заботы по его уничтожению
MyThread.FreeOnTerminate:= True;

// Пример передачи входных параметров через поля объекта-потока, в точке
// создания экземпляра, когда он еще не запущен.
// Лично я, предпочитаю делать это через параметры переопределяемого
// конструктора (tMyThread.Create)
MyThread.Param2:= StrToInt(Form1.Edit2.Text);

MyThread.Stopped:= False; // своего рода тоже параметр, но меняющийся во
// время работы потока
except
// поскольку поток еще не запущен и не сможет самоуничтожиться, уничтожим его "вручную"
FreeAndNil(MyThread);
// а дальше пусть исключительная ситуация обрабатывается обычным порядком
raise;
end;

// Поскольку объект потока успешно создан и настроен, настало время запустить его
MyThread.Resume;

ShowMessage("Поток запущен");
end;

procedure TForm1.btnStopClick(Sender: TObject);
begin
// Если экземпляр потока еще существует, то попросим его остановиться
// Причем, именно "попросим". "Заставить" в принципе тоже можем, но это будет
// исключительно аварийный вариант, требующий четкого понимания всей этой
// потоковой кухни. Поэтому, здесь не рассматривается.
if Assigned(MyThread) then
MyThread.Stopped:= True
else
ShowMessage("Поток не запущен!");
end;

procedure TForm1.EventOnSendMessageMetod(var Msg: TMessage);
begin
// метод обработки синхронного сообщения
// в WParam адрес объекта tMyThread, в LParam тек.значение LastRandom потока
with tMyThread(Msg.WParam) do begin
Form1.Label3.Caption:= Format("%d %d %d",);
end;
end;

procedure TForm1.EventOnPostMessageMetod(var Msg: TMessage);
begin
// метод обработки асинхронного сообщения
// в WParam тек.значение IterationNo, в LParam тек.значение LastRandom потока
Form1.Label4.Caption:= Format("%d %d",);
end;

procedure TForm1.EventMyThreadOnTerminate (Sender:tObject);
begin
// ВАЖНО!
// Метот обработки события OnTerminate всегда вызывается в контексте основного
// потока - это гарантируется реализацией tThread. Поэтому, в нем можно свободно
// использовать любые свойства и методы любых объектов

// На всякий случай, убедимся что экземпляр объекта еще существует
if not Assigned(MyThread) then Exit; // если его нет, то и делать нечего

// получение результатов работы потока экземпляра объекта потока
Form1.Memo1.Lines.Add(Format("Поток завершился с результатом %d",));
Form1.Memo1.Lines.AddStrings((Sender as tMyThread).ResultList);

// Уничтожение ссылки на экземпляр объекта потока.
// Поскольку поток у нас самоуничтожающийся (FreeOnTerminate:= True)
// то после завершения обрабтчика OnTerminate, экземпляр объекта-потока будет
// уничтожен (Free), и все ссылки на него станут недействительными.
// Что бы случайно не напороться на такую ссылку, затрем MyThread
// Еще раз замечу - не уничтожим объект, а только затрем ссылку. Объект
// уничтожится сам!
MyThread:= Nil;
end;

constructor tMyThread.Create (aParam1:String);
begin
// Создаем экземпляр ПРИОСТАНОВЛЕННОГО потока (см.коментарий при создании экземпляра)
inherited Create(True);

// Создание внутренних объектов (если необходимо)
ResultList:= tStringList.Create;

// Получение исходных данных.

// Копирование входных данных переданных через параметр
Param1:= aParam1;

// Пример получения входных данных из VCL-компонентов в конструкторе объекта-потока
// Такое в данном случае допустимо, поскольку конструктор вызывается в контексте
// основного потока. Следовательно, здесь можно обращаться к VCL-компонентам.
// Но, я такого не люблю, поскольку считаю что плохо когда поток знает что-то
// о какой-то там форме. Но, чего не сделаешь для демонстрации.
Param3:= Form1.CheckBox1.Checked;
end;

destructor tMyThread.Destroy;
begin
// уничтожение внутренних объектов
FreeAndNil(ResultList);
// уничтожение базового tThread
inherited;
end;

procedure tMyThread.Execute;
var
t:Cardinal;
s:String;
begin
IterationNo:= 0; // счетчик результатов (номер цикла)

// В моем примере тело потока представляет собой цикл, который завершается
// либо по внешней "просьбе" завершиться передаваемый через изменяемый параметр Stopped,
// либо просто совершив 5 циклов
// Мне приятнее такое записывать через "вечный" цикл.

While True do begin

Inc(IterationNo); // очередной номер цикла

LastRandom:= Random(1000); // слючайное число - для демонстрации передачи параметров от потока в форму

T:= Random(5)+1; // время на которое будем засыпать если нас не завершат

// Тупая работа (зависящая от входного параметра)
if not Param3 then
Inc(Param2)
else
Dec(Param2);

// Сформируем промежуточный результат
s:= Format("%s %5d %s %d %d",
);

// Добавим промежуточный результат к списку резуольтатов
ResultList.Add(s);

//// Примеры передачи промежуточного результата на форму

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

//// Достоинства:
//// - стандартность и универсальность
//// - в синхронизированном методе можно пользоваться
//// всеми полями объекта-потока.
// сначала, если необходимо, надо сохранить передаваемые данные в
// специальных полях объекта объекта.
SyncDataN:= IterationNo;
SyncDataS:= "Sync"+s;
// и затем обеспечить синхронизированный вызов метода
Synchronize(SyncMetod1);

//// Передача через синхронную отсылку сообщения (SendMessage)
//// в этом случае, данные можно передать как через параметры сообщения (LastRandom),
//// так и через поля объекта, передав в параметре сообщения адрес экземпляра
//// объекта-потока - Integer(Self).
//// Недостатки:
//// - поток должен знать handle окна формы
//// - как и при Synchronize, текущий поток будет приостановлен до
//// завершения обработки сообщения основным потоком
//// - требует существенных затрат процессорного времени на каждый вызов
//// (на переключение потоков) поэтому нежелателен очень частый вызов
//// Достоинства:
//// - как и при Synchronize, при обработке сообщения можно пользоваться
//// всеми полями объекта-потока (если конечно был передан его адрес)


//// запуска потока.
SendMessage(Form1.Handle,WM_USER_SendMessageMetod,Integer(Self),LastRandom);

//// Передача через асинхронную отсылку сообщения (PostMessage)
//// Поскольку в этом случае к моменту получения сообщения основным потоком,
//// посылающий поток может уже завершиться, передача адреса экземпляра
//// объекта-потока недопустима!
//// Недостатки:
//// - поток должен знать handle окна формы;
//// - из-за асинхронности, передача данных возможна только через параметры
//// сообщения, что существенно усложняет передачу данных имеющих размер
//// более двух машинныхх слов. Удобно применять для передачи Integer и т.п.
//// Достоинства:
//// - в отличие от предыдущих методов, текущий поток НЕ будет
//// приостановлен, а сразу же продолжит свое выполнение
//// - в отличии от синхронизированного вызова, обработчиком сообщения
//// является метод формы, который должен иметь знания об объекте-потоке,
//// или вовсе ничего не знать о потоке, если данные передаеются только
//// через параметры сообщения. Т.е., поток может ничего не знать о форме
//// вообще - только ее Handle, который может быть передан как параметр до
//// запуска потока.
PostMessage(Form1.Handle,WM_USER_PostMessageMetod,IterationNo,LastRandom);

//// Проверка возможного завершения

// Проверка завершения по параметру
if Stopped then Break;

// Проверка завершения по случаю
if IterationNo >= 10 then Break;

Sleep(t*1000); // Засыпаем на t секунд
end;
end;

procedure tMyThread.SyncMetod1;
begin
// этот метод вызывается посредством метода Synchronize.
// Т.е., не смотря на то что он является методом потока tMyThread,
// он выполняется в контексте основного потока приложения.
// Следовательно, ему все можно, ну или почти все:)
// Но помним, здесь не стоит долго "возиться"

// Переданные параметры, мы можем извлечь из специальных поле, куда мы их
// сохранили перед вызовом.
Form1.Label1.Caption:= SyncDataS;

// либо из других полей объекта потока, например отражающих его тек.состояние
Form1.Label2.Caption:= Format("%d %d",);
end;

А вообще, примеру предшествовали следующие мои рассуждения на тему....

Во первых:
ВАЖНЕЙШЕЕ правило многопоточного программирования на Delphi:
В контексте не основного потока нельзя, обращаться к свойствам и методам форм, да и вообще всех компонентов которые "растут" из tWinControl.

Это означает (несколько упрощенно) что ни в методе Execute унаследованного от TThread, ни в других методах/процедурах/функциях вызываемых из Execute, нельзя напрямую обращаться ни к каким свойствам и методам визуальных компонентов.

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

Если коротенько на пальцах:

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

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

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

Существуют и другие случаи, но реже. Впрочем, это и не важно. Сейчас не об этом.

Теперь, как все это пишется. Естественно рассматривается некий наиболее частый случай, несколько обобщенный. Итак.

Работа, выносимая в отдельный поток, в общем случае имеет четыре сущности (уж и не знаю как назвать точнее):
1. Исходные данные
2. Собственно сама работа (она может зависеть от исходных данных)
3. Промежуточные данные (например, информация о текущем состоянии выполнения работы)
4. Выходные данные (результат)

Чаще всего для считывания и вывода большей части данных используются визуальные компоненты. Но, как было сказано выше – нельзя из потока напрямую обращаться к визуальным компонентам. Как же быть?
Разработчики Delphi предлагают использовать метод Synchronize класса TThread. Здесь я не буду описывать то, как его применять – для этого есть вышеупомянутая статья. Скажу лишь, что его применение, даже правильное, не всегда оправдано. Имеются две проблемы:

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

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

Причем, обе проблемы взаимосвязаны, и вызывают противоречие: с одной стороны, для решения первой, надо «размельчать» методы вызываемые через Synchronize, а с другой, их тогда чаще приходится вызывать, теряя драгоценный процессорный ресурс.

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

Исходные данные
Все данные которые передаются в поток, и не изменяются во время его работы, нужно передавать еще до его запуска, т.е. при создании потока. Для их использования в теле потока, нужно сделать их локальную копию (обычно в полях потомка TThread).
Если есть исходные данные которые могут меняться во время работы потока, то доступ к таким данным нужно осуществлять либо через синхронизируемые методы (методы вызываемые через Synchronize), либо через поля объекта-потока (потомка TThread). Последнее требует определенной осторожности.

Промежуточные и выходные данные
Здесь, опять же есть несколько способов (в порядке моих предпочтений):
- Метод асинхронной отсылки сообщений главному окну приложению.
Используется обычно для отсылки основному окну приложения сообщений о состоянии протекания процесса, с передачей незначительного объема данных (например, процента выполнения)
- Метод синхронной отсылки сообщений главному окну приложению.
Используется обычно для тех же целей что и асинхронная отсылка, но позволяет передать больший объем данных, без создания отдельной копии.
- Синхронизируемые методы, по возможности, объединяя в один метод передачу как можно большего объема данных.
Можно использовать и для получения данных с формы.
- Через поля объекта-потока, обеспечением взаимоисключающего доступа.
Подробнее, можно почитать в статье.

Эх. Коротенько опять не получилось

конец файла . Таким образом, записи в логе, выполняемые разными процессами, никогда несмешиваются. В более современныхUnix-системах для ведения логов предоставляется специальный сервис syslog(3C) .

Преимущества:

  1. Простота разработки. Фактически, мы запускаем много копий однопоточного приложения и они работают независимо друг от друга. Можно не использовать никаких специфически многопоточных API и средств межпроцессного взаимодействия .
  2. Высокая надежность. Аварийное завершение любого из процессов никак не затрагивает остальные процессы.
  3. Хорошая переносимость. Приложение будет работать налюбой многозадачной ОС
  4. Высокая безопасность. Разные процессы приложения могут запускаться от имени разных пользователей. Таким образом можно реализовать принцип минимальных привилегий, когда каждый из процессов имеет лишь те права, которые необходимы ему для работы. Даже если в каком-то из процессов будет обнаружена ошибка, допускающая удаленное исполнение кода, взломщик сможет получить лишь уровень доступа, с которым исполнялся этот процесс.

Недостатки:

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

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

Примеры: apache 1.x ( сервер HTTP )

Многопроцессные приложения, взаимодействующие через сокеты, трубы и очереди сообщений System V IPC

Перечисленные средства IPC ( Interprocess communication ) относятся к так называемым средствам гармонического межпроцессного взаимодействия. Онипозволяют организовать взаимодействие процессов и потоков без использования разделяемой памяти. Теоретики программирования очень любят эту архитектуру, потому что она практически исключает многие варианты ошибок соревнования.

Преимущества:

  1. Относительная простота разработки.
  2. Высокая надежность. Аварийное завершение одного из процессов приводит к закрытию трубы или сокета, а в случае очередей сообщений – к тому, что сообщения перестают поступать в очередь или извлекаться из нее. Остальные процессы приложения легко могут обнаружить эту ошибку и восстановиться после нее, возможно (но не обязательно) просто перезапустив отказавший процесс.
  3. Многие такие приложения (особенно основанные на использовании сокетов) легко переделываются для исполненияв распределенной среде, когда разные компоненты приложения исполняются на разных машинах.
  4. Хорошая переносимость. Приложение будет работать на большинстве многозадачных ОС, в том числе на старых Unix-системах.
  5. Высокая безопасность. Разные процессы приложения могут запускаться от имени разных пользователей. Таким образом можно реализовать принцип минимальных привилегий, когда каждый из процессов имеет лишь те права, которые необходимы ему для работы.

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

Недостатки:

  1. Не для всех прикладных задач такую архитектуру легко разработать и реализовать.
  2. Все перечисленные типы средств IPC предполагают последовательную передачу данных. Если необходим произвольный доступ к разделяемым данным, такая архитектура неудобна.
  3. Передача данных через трубу, сокет и очередь сообщений требует исполнения системных вызовов и двойного копирования данных – сначала из адресного пространства исходного процесса в адресное пространство ядра, затем из адресного пространства ядра в память целевого процесса . Это дорогие операции. При передаче больших объемов данных это может превратиться в серьезную проблему.
  4. В большинстве систем действуют ограничения на общее количество труб, сокетов и средств IPC. Так, в Solaris по умолчанию допускается не более 1024 открытых труб, сокетов и файлов на процесс (это обусловлено ограничениями системного вызова select). Архитектурное ограничение Solaris – 65536 труб, сокетов и файлов на процесс.

    Ограничение на общее количество сокетов TCP/IP – не более 65536 на сетевой интерфейс (обусловлено форматом заголовков TCP). Очереди сообщений System V IPC размещаются вадресном пространствеядра, поэтому действуют жесткиеограничения на количество очередей в системе и на объем и количество одновременно находящихся в очередях сообщений.

  5. Создание и уничтожение процесса, а также переключение между процессами – дорогие операции. Не во всех случаях такая архитектура оптимальна.

Многопроцессные приложения, взаимодействующие через разделяемую память

В качестве разделяемой памяти может использоваться разделяемая память System V IPC и отображение файлов на память . Для синхронизации доступа можно использовать семафоры System V IPC , мутексы и семафоры POSIX , при отображении файлов на память – захват участков файла.

Преимущества:

  1. Эффективный произвольный доступ к разделяемым данным. Такая архитектура пригодна для реализации серверов баз данных.
  2. Высокая переносимость. Может быть перенесено налюбую операционную систему, поддерживающую или эмулирующую System V IPC .
  3. Относительно высокая безопасность. Разные процессыприложениямогут запускаться от имени разных пользователей. Таким образом можно реализовать принцип минимальных привилегий, когда каждый из процессов имеет лишь те права, которые необходимы ему для работы. Однако разделение уровней доступа не такое жесткое, как в ранее рассмотренных архитектурах.

Недостатки:

  1. Относительная сложность разработки. Ошибки при синхронизации доступа – так называемые ошибки соревнования – очень сложно обнаруживать при тестировании.

    Это может привести к повышению общей стоимости разработки в 3–5 раз по сравнению с однопоточными или более простыми многозадачными архитектурами.

  2. Низкая надежность. Аварийное завершение любого из процессов приложения может оставить (и часто оставляет) разделяемую память в несогласованном состоянии.

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

  3. Создание и уничтожение процесса и переключение между ними – дорогие операции.

    Поэтому данная архитектура оптимальна не для всех приложений.

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

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

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

Фактически, данная архитектура сочетает недостатки многопроцессных и собственно многопоточных приложений. Тем не менее, ряд популярных приложений, разработанных в 80е и начале 90х, до того, как в Unix были стандартизованы многопоточные API , используют эту архитектуру. Это многие серверы баз данных, как коммерческие ( Oracle , DB2 , Lotus Domino), такисвободно распространяемые,современные версии Sendmail инекоторые другие почтовые серверы.

Собственно многопоточные приложения

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

Поэтому потоки должны использовать специальные средства для организации взаимодействия. Наиболее важные средства – это примитивы взаимоисключения (мутексы и блокировки чтения-записи). Используя эти примитивы, программист может добиться того, чтобы ни один поток не обращался к разделяемым ресурсам, пока они находятся в несогласованном состоянии (это и называется взаимоисключением). System V IPC , разделяются только те структуры, которые размещены в сегменте разделяемой памяти. Обычные переменные и размещаемые обычным образом динамические структуры данных свои укаждого изпроцессов). Ошибки придоступекразделяемым данным – ошибки соревнования – очень сложно обнаруживать при тестировании.

  • Высокая стоимость разработки и отладки приложений, обусловленная п. 1.
  • Низкая надежность. Разрушение структур данных, например в результате переполнения буфера или ошибок работы с указателями, затрагивает все нити процесса и обычно приводит к аварийному завершению всего процесса. Другие фатальные ошибки, например, деление на ноль в одной из нитей, также обычно приводят к аварийной остановке всех нитей процесса.
  • Низкая безопасность. Все нити приложения исполняются в одном процессе, то есть от имени одного и того же пользователя и с одними и теми же правами доступа. Невозможно реализовать принцип минимума необходимых привилегий, процесс должен исполняться от имени пользователя, который может исполнять все операции, необходимые всем нитям приложения.
  • Создание нити – все-таки довольно дорогая операция. Для каждой нити в обязательном порядке выделяется свой стек, который по умолчанию занимает 1 мегабайт ОЗУ на 32битных архитектурах и 2 мегабайта на 64-битных архитектурах, и некоторые другие ресурсы. Поэтому данная архитектура оптимальна не для всех приложений.
  • Невозможность исполнять приложение на многомашинном вычислительном комплексе. Упоминавшиеся в предыдущем разделе приемы, такие, как отображение на память разделяемых файлов, для многопоточной программы не применимы.
  • В целом можно сказать, что многопоточные приложения имеют почти те же преимущества и недостатки, что и многопроцессные приложения, использующие разделяемую память .

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