Многопоточные приложения
- Знакомство
с многопоточностью - Домены приложений
- Создание потоков
- Метод Join
- Имена потоков, CurrentThread и ThreadState
- Окно потоков
- Приостановка потока
- Завершение или прерывание программных потоков
- Приостановка и уничтожение потоков
- Фоновые потоки (демоны)
- Более серьезный пример: извлечение данных из кода HTML
- Главная опасность (общие данные)
- Решение проблемы: синхронизация
- Команда SyncLock и класс Monitor
- Взаимная блокировка
- Совместная работа с данными по мере их создания
- Многопоточность в графических программах
- Следующий шаг: кнопка Show Count
- Почему при многопоточном программировании возникает столько проблем?
Многозадачность
в современных операционных
системах воспринимается как нечто
само собой разумеющееся [ До
появления Apple OS X на компьютерах
Macintosh не было современных
многозадачных операционных систем.
Правильно спроектировать
операционную систему с полноценной
многозадачностью очень трудно,
поэтому за основу OS X пришлось взять
систему Unix. ]. Пользователь
рассчитывает на то, что при
одновременном запуске текстового
редактора и почтового клиента эти
программы не будут конфликтовать, а
при приеме электронной почты
редактор не перестанет работать.
При одновременном запуске
нескольких программ операционная
система быстро переключается между
программами, по очереди
предоставляя им процессор (если,
конечно, на компьютере не
установлено несколько процессоров).
В результате создается иллюзия одновременной
работы нескольких программ,
поскольку даже лучшая машинистка (и
самое быстрое Интернет-соединение)
не угонится за современным
процессором.
Многопоточность
(multithreading) в каком-то смысле можно
рассматривать как следующий
уровень многозадачности: вместо
того, чтобы переключаться между
разными программами, операционная
система переключается между
разными частями одной программы.
Например, многопоточный почтовый
клиент позволяет принимать новые
сообщения электронной почты во
время чтения или составления новых
сообщений. В наше время
многопоточность тоже
воспринимается многими
пользователями как должное.
В VB нормальной
поддержки многопоточности не было
никогда. Правда, в VB5 появилась одна
из ее разновидностей — совместная
потоковая модель (apartment threading). Как
вы вскоре увидите, совместная
модель предоставляет в
распоряжение программиста часть
преимуществ многопоточности, но не
позволяет использовать все
возможности в полной мере. Рано или
поздно с учебной машины приходится
пересаживаться на настоящую, и VB .NET
стал первой версией VB с поддержкой
свободной многопоточной модели.
Тем не менее
многопоточность не принадлежит к
числу возможностей, которые легко
реализуются в языках
программирования и легко
осваиваются программистами. Почему?
Потому что в
многопоточных приложениях могут
возникать очень хитрые ошибки,
которые непредсказуемо появляются
и исчезают (а такие ошибки труднее
всего отлаживать).
Честно
предупреждаем: многопоточность —
одна из сложнейших областей
программирования. Малейшая
невнимательность приводит к
появлению неуловимых ошибок, на
исправление которых уходят
астрономические суммы. По этой
причине в настоящей главе
приведено немало плохих примеров
— мы намеренно написали их так,
чтобы продемонстрировать
характерные ошибки. В этом и
состоит самый безопасный подход к
изучению многопоточного
программирования: вы должны уметь
разглядеть потенциальные проблемы,
когда на первый взгляд все работает
нормально, и знать пути их решения.
Если вы хотите использовать приемы
многопоточного программирования,
без этого не обойтись.
Но каким бы
опасным ни было многопоточное
программирование, при
профессиональном решении
некоторых задач оно незаменимо.
Если ваши программы не будут
использовать многопоточность там,
где это уместно, пользователи
сильно разочаруются и предпочтут
другой продукт. Например, лишь в
четвертой версии популярной
почтовой программы 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 [Assembly
]
Assemblies = theDomain.GetAssemblies
Dim anAssemblyxAs [Assembly ]
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 должна работать
в отдельном потоке.
На рис. 10.1
показан пример того, что может
произойти после запуска программы
и ее последующего прерывания
клавишей Ctrl+Break. В нашем случае
новый поток запустился лишь после
того, как счетчик в главном потоке
увеличился до 341!
Рис. 10.1. Простая
многопоточная программно время
работы
Если программа
будет работать в течение
большегошромежутка времени,
результат будет выглядеть примерно
так, как показано на рис. 10.2. Мы
видим, что выполнение запущенного
потока приостанавливается и
управление снова передается
главному потоку. В данном случае
имеет место проявление вытесняющей
мно-гопоточности посредством
квантования времени. Смысл этого
устрашающего термина разъясняется
ниже.
Рис. 10.2.
Переключение между потоками в
простой многопоточной программе
При прерывании
потоков и передаче управления
другим потокам операционная
система использует принцип
вытесняющей многопоточности
посредством квантования времени.
Квантование времени также решает
одну из распространенных проблем,
возникавших прежде в многопоточных
программах, — один поток занимает
все процессорное время и не
уступает управления другим потокам
(как правило, это случается в
интенсивных циклах вроде
приведенного выше). Чтобы
предотвратить монопольный захват
процессора, ваши потоки должны
время от времени передавать
управление другим потокам. Если
программа окажется «несознательной»,
существует другое, чуть менее
желательное решение: операционная
система всегда вытесняет
работающий поток независимо от
уровня его приоритета, чтобы доступ
к процессору был предоставлен
каждому потоку в системе.
Если включить
следующую строку в нашу программу
перед вызовом 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 в
противном случае.
Имена потоков,
CurrentThread и ThreadState
Перед запуском
каждому потоку рекомендуется
присвоить содержательное имя,
поскольку имена значительно
упрощают отладку многопоточных
программ. Для этого следует задать
значение свойства Name командой
следующего вида:
bThread.Name =
"Subtracting thread"
Свойство 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 tie As
ThreadlnterruptedException
' Пассивное состояние потока было прервано
Catch e As Exception
'Остальные исключения
End Try
Завершение или
прерывание программных потоков
Поток
автоматически завершается при
выходе из метода, указанного при
создании делегата 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 — поток обрабатывает
исключение по своему усмотрению.
Приостановка и уничтожение потоков
Пространство
имен 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("<b>Amazon.com
Sales Rank:</b>")
_
+ "<b>Amazon.com Sales Rank:</b>".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 практически не
будет отличаться от ограниченных
возможностей 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 легко реализуются
условные переменные. Для этого
потребуется лишь синхронизировать
запись в общее логическое свойство,
доступное для чтения и записи, как
это сделано в следующем фрагменте:
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
Взаимная блокировка
В процессе
синхронизации блокировка
устанавливается для объектов, а не
потоков, поэтому при использовании разных
объектов для блокировки разных фрагментов
кода в программах иногда возникают
весьма нетривиальные ошибки. К
сожалению, во многих случаях
синхронизация по одному объекту
просто недопустима, поскольку она
приведет к слишком частой
блокировке потоков.
Рассмотрим
ситуацию взаимной блокировки (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.
- Строки
26 и 27 просто выводят на консоль
отладочную информацию. В
основном коде метода (строки 28-36)
доступ к вилке
синхронизируется по объектной
переменной Me. Поскольку в нашей
программе используется только
одна вилка, синхронизация по Me
гарантирует, что два потока не
смогут одновременно захватить
ее. Команда Slee'p (в блоке,
начинающемся в строке 34)
имитирует задержку между
захватом вилки/ножа и началом
еды. Учтите, что команда Sleep не
снимает блокировку с объектов
и лишь ускоряет возникновение
взаимной блокировки!
Однако наибольший интерес представляет код класса Programmer (строки 61-108). В строках 67-70 определяется общий конструктор, что гарантирует наличие в программе только одной вилки и ножа. Код свойств (строки 74-94) прост и не требует комментариев. Самое главное происходит в методе Eat, выполняемом двумя отдельными потоками. Процесс продолжается в цикле до тех пор, пока какой-либо поток не захватит вилку вместе с ножом. В строках 98-102 объект случайным образом захватывает вилку/нож, используя вызов Rnd, — именно это и порождает взаимную блокировку. Происходит следующее:
Поток, выполняющий метод Eat объекта Тот, активизируется и входит в цикл. Он захватывает нож и переходит в состояние ожидания.
- Поток,
выполняющий метод Eat объекта Bob,
активизируется и входит в цикл.
Он не может захватить нож, но
захватывает вилку и переходит
в состояние ожидания.
- Поток,
выполняющий метод Eat объекта
Тот, активизируется и входит в
цикл. Он пытается захватить
вилку, однако вилка уже
захвачена объектом Bob; поток
переходит в состояние ожидания.
- Поток, выполняющий метод Eat объекта Bob, активизируется и входит в цикл. Он пытается захватить нож, однако нож уже захвачен объектом Тот; поток переходит в состояние ожидания.
Все
это продолжается до бесконечности
— перед нами типичная ситуация
взаимной блокировки (попробуйте
запустить программу, и вы убедитесь
в том, что поесть так никому и не
удается).
О возникновении взаимной
блокировки можно узнать и в окне
потоков. Запустите программу и
прервите ее клавишами Ctrl+Break.
Включите в окно просмотра
переменную Me и откройте окно
потоков. Результат выглядит
примерно так, как показано на рис. 10.7.
Из рисунка видно, что поток Bob
захватил нож, но вилки у него нет.
Щелкните правой кнопкой мыши в окне
потоков на строке Тот и выберите в
контекстном меню команду Switch to Thread.
Окно просмотра показывает, что у
потока Тот имеется вилка, но нет
ножа. Конечно, это не является
стопроцентным доказательством, но
подобное поведение по крайней мере
заставляет заподозрить неладное.
Рис. 10.7.
Анализ взаимной блокировки в окне
потоков
Следовательно,
если убрать вызов Rnd в строке 98 и
заменить его фрагментом
mFork.GrabFork(Me)
mKnife.GrabKnife(Me)
взаимная
блокировка исчезает!
Совместная работа с данными по мере их создания
В многопоточных
приложениях часто встречается
ситуация, когда потоки не только
работают с общими данными, но и
ожидают их появления (то есть поток
1 должен создать данные, прежде чем
поток 2 сможет их использовать).
Поскольку данные являются общими,
доступ к ним необходимо
синхронизировать. Также необходимо
предусмотреть средства для
оповещения ожидающих потоков о
появлении готовых данных.
Подобная
ситуация обычно называется проблемой
«поставщик/потребитель». Поток
пытается обратиться к данным,
которых еще нет, поэтому он должен
передать управление другому потоку,
создающему нужные данные. Проблема
решается кодом следующего вида:
- Поток
1 (потребитель) активизируется,
входите синхронизированный
метод, ищет данные, не находит
их и переходит в состояние
ожидания. Предварителъно он
должен снять блокировку, чтобы
не мешать работе потока-
поставщика.
- Поток
2 (поставщик) входит в
синхронизированный метод,
освобожденный потоком 1, создает
данные для потока 1 и каким-то
образом оповещает поток 1 о
наличии данных. Затем он
снимает блокировку, чтобы
поток 1 смог обработать новые
данные.
Связи «поставщик/потребитель»
встречаются очень часто, поэтому в
библиотеках классов
многопоточного программирования
для таких ситуаций создаются
специальные примитивы. В .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 она не позволяет вернуть
код завершения операционной
системе).
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 m_CountDone As Boolean
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
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
С двумя
кнопками на форме связывается
весьма простой код. В процедуре 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
Вполне вероятно,
что в некоторых случаях этот код
будет работать. Тем не менее:
- Взаимодействие
вторичного потока с потоком,
создающим графический
интерфейс, не удается
организовать очевидными средствами.
- Никогда
не изменяйте элементы в
графических программах из
других программных потоков.
Все изменения должны
происходить только в потоке,
создавшем графический
интерфейс.
Если вы
нарушите эти правила, мы гарантируем,
что в ваших многопоточных
графических программах будут
возникать тонкие, неуловимые
ошибки.
И все же здравый
смысл подсказывает, что в
графических приложениях должны
существовать средства модификации
элементов из другого потока. В .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.
Компилятор (или даже
специализированное устройство
управления памятью) может просто
создать массив, а потом заполнить
его одной операцией копирования
вместо многократного копирования
отдельных элементов! ].
Надеемся, эти пояснения помогут вам лучше понять, почему многопоточное программирование порождает столько проблем, — или по крайней мере меньше удивляться при виде странного поведения ваших многопоточных программ!