Многопотоковость в visual basic


Содержание

Многопотоковость в visual basic

tyomitch » 17.05.2006 (Ср) 11:31

Хочется развеять ряд распространённых заблуждений.

* VB6 поддерживает многопоточные программы. В частности, на VB можно — безо всякого шаманства — написать многопоточный COM-сервер (как в виде ActiveX EXE, так и в виде ActiveX DLL).

Для обеспечения этого рантайм написан исключительно multithreading-aware способом: все «глобальные» данные на самом деле хранятся в TLS. Отсутствие по-настоящему глобальных данных обеспечивает защиту от race-condition-ов и других ошибок синхронизации.

* Написать на VB6 многопоточную программу намного сложнее , чем просто вызвать CreateThread(AddressOf ThreadProc), проверить под >работает! вот чудо! ), откомпилировать в P-Code и сделать вид, что всё в порядке.

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

Поскольку скомпилированная в Native Code программа расчитывает обнаружить в TLS все свои «глобальные» данные, а нить, созданная по CreateThread, не содержит TLS, то такой наивный способ запустить новую нить в скомпилированном коде не работает.

* И всё же написать на VB6 нормальную многопоточную программу возможно : для этого придётся в лучших традициях процедуры bootstrap (поднятие себя за шнурки ботинков) из созданной нити инициализировать TLS вручную. (Код на VB не может работать без TLS; как же он сам создаст себе TLS? Оказывается, сможет.)

Такие трюки, однако, требуют большого геморроя c хитрым API, который непременно нужно объявлять в TLB — иначе магия не сработает.

* Если многопоточное VB6-приложение написано правильно, то глобальных данных — а значит, и способов обмена данными между нитями — нет вовсе . Координация нитей в таком приложении сопряжена с большими сложностями. (Можно, например, использовать атомы: они глобальные для оконной станции.)

Можно использовать объектные вызовы между нитями, но это именно то, чего лучше избегать: при таком вызове вызывающая нить замораживается до освобождения вызываемой — фактически, программа превращается обратно в однопоточную. Да и откуда во вновь созданной нити взять ссылку на объект из другой нити? Ведь передать её не в чем.

* Если у вас после всего сказанного осталось желание писать на VB6 многопоточную прогу — подумайте ещё раз . Многопоточность — не магия, которая решит за программиста вопросы проектирования программы. Если у вас зависает однопоточная программа, то зависнет и многопоточная.

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

Потоки в Visual Basic (стр. 1 из 4)

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

Проблема не в Visual Basic или в технологии. Проблема в том, что большинство авторов применяют одно и тоже правило к AddressOf методикам, что большинство компаний по разработке ПО считают, что если Вы должны что-то сделать, то Вы сможете. Идея о том, что применение самой новой и последней технологии должно, по определению, быть самым лучшим решением проблемы, широко распространена в индустрии ПО. Эта идея неверна. Развертывание технологии должно управляться прежде всего проблемой, которую необходимо решить решить, а не технологией, которую кто-то пробует Вам впарить;).

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

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

Недавние статьи в Microsoft Systems Journal и Visual Basic Programmer’s Journal представили программистам на Visual Basic возможность использования функции API CreateThread, чтобы непосредственно поддерживать многопоточный режим под Visual Basic. После этого, один читатель пожаловался, что моя книга Visual Basic Programmer’s Guide to the Win32 API является неполной, потому что я не описал в ней эту функцию и не продемонстрировал эту технологию. Эта статья — частично является ответом этому читателю, и частично — ответом на другие статьи, написанными на эту тему. Эта статья также является дополнением к главе 14 моей книги «Разработка ActiveX компонент на Visual Basic 5.0» относительно новых возможностей, обеспечиваемых Visual Basic 5.0 Service Pack 2.

Быстрый обзор Многопоточности

Если Вы уже хорошо разбираетесь в технологии многопоточного режима, то Вы можете пропустить этот раздел и продолжать чтение с раздела, названного «Что нового в Service Pack 2.»

Каждый, кто использует Windows, знает, что Windows способно делать больше чем одну вещь одновременно. Может одновременно выполнять несколько программ, при одновременном проигрывании компакт-диска, посылке факса и пересылке файлов. Каждый программист знает (или должен знать) что ЦЕНТРАЛЬНЫЙ ПРОЦЕССОР компьютера может только выполнять одну команду одновременно (проигнорируем существование многопроцессорных машин). Как единственный ЦЕНТРАЛЬНЫЙ ПРОЦЕССОР может выполнять множество задач?

Это делается быстрым переключением между многими задачами. Операционная система содержит в памяти все программы, которые запущены в настоящий момент. Это позволяет ЦЕНТРАЛЬНОМУ ПРОЦЕССОРУ выполнять программы по очереди. Каждый раз происходит переключение между программами, при этом меняется содержимое внутренних регистров, включая указатель команды и указатель вершины стека. Каждая из таких «задач» называется потоком выполнения (thread of execution).

В простой многозадачной системе, каждая программа имеет емеет единственный поток. Это означает, что ЦЕНТРАЛЬНЫЙ ПРОЦЕССОР начинает выполнение команд в начале программы и продолжает следуя инструкциям в последовательности, определенной программой до тех пор, пока программа не завершается.

Скажем, программа имеет пять команд: B C D и E, которые выполняются последовательно (никаких переходов нет в этом примере). Когда приложение имеет один поток, команды будут всегда выполнять в точно том же самом порядке: A, B, C, D и E. Действительно, ЦЕНТРАЛЬНЫЙ ПРОЦЕССОР может потребовать времени для выполнения других команд в других программах, но они не будут влиять на это приложение, если не имеется конфликт над общими ресурсами системы, но это уже отдельная тема для разговора.

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

В каком порядке будут следовать команды в этом приложении?

Это могло бы быть:

Thread 1 A B C D E

Thread 1 A B C D E

Thread 1 A B C D E

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

Почему — это проблема?

Рассмотрим проект MTDemo:

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

‘ MTDemo — Multithreading Demo program

‘ Copyright © 1997 by Desaware Inc. All Rights Reserved

Public GenericGlobalCounter As Long

Public TotalIncrements As Long

‘ Этот проект содержит одну форму — frmMTDemo1, которая содержит

‘ MTDemo — Multithreading Demo program ‘ Copyright © 1997 by Desaware Inc. All Rights Reserved

Dim State As Integer

‘ State = 1 — Loading existing value

‘ State = 2 — Adding 1 to existing value

‘ State = 3 — Storing existing value

‘ State = 4 — Extra delay

Dim Accumulator As Long

Const OtherCodeDelay = 10

Private Sub Command1_Click()

Dim f As New frmMTDemo1

Private Sub Form_Load()

Timer1.Interval = 750 + Rnd * 500

Private Sub Timer1_Timer()

Select Case State

lblOperation = «Loading Acc»

Accumulator = Accumulator + 1

TotalIncrements = TotalIncrements + 1

lblOperation = «Generic Code»

If otherdelay >= OtherCodeDelay Then

otherdelay = otherdelay + 1

Public Sub UpdateDisplay()

Эта программа для моделирования многопоточного режима использует таймер и простой конечный автомат. Переменная State описывает пять команд, которые эта программа выполняет. State = 0 — неактивное состояние. State = 1 загружает локальную переменную глобальной переменной GenericGlobalCounter. State = 2 увеличивает на единицу локальную переменную. State = 3 запоминает результат в переменной GenericGlobalCounter и увеличивает переменную TotalIncrements (которая считает количество приращений переменной GenericGlobalCounter). State = 3 добавляет дополнительную задержку, представляющую собой время, затраченное на выполнение других команд в программе.

Функция UpdateDisplay обновляет три метки на форме, которые показывают текущее значение переменной GenericGlobalCounter, локального сумматора, и общего количества приращений.

Каждый сигнал таймера моделирует цикл ЦЕНТРАЛЬНОГО ПРОЦЕССОРА в текущем потоке. Если Вы запустите программу, то увидете, что значение переменной GenericGlobalCounter будет всегда точно равно переменной TotalIncrements, потому что переменная TotalIncrements показывает количество увеличений счетчика GenericGlobalCounter потоком.

Но что случится, когда Вы нажимаете кнопку Command1 и запустите второй экземпляр формы? Эта новая форма смоделирует второй поток.

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

Что, если переменная представляет объектный счет блокировки — который следит, когда объект должен быть освобожден? Что, если она представляет собой сигнал, который указывает, что ресурс находится в использовании?

Такая проблема может привести к появлению ресурсов, постоянно недоступных в системе, к объекту, блокируемому в памяти, или преждевременно освобожденному. Это может привести к сбоям приложения.

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

Решение проблем Многопоточности

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

Избегайте всеобщего использования глобальных переменных.

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

Первый подход используется в основном в Visual Basic. Когда Вы включаете многопоточный режим в Visual Basic приложения, все глобальные переменные станут локальными для специфического потока. Это свойственно способу, с которым Visual Basic выполняет apartment model threading — подробнее об этом позднее.

Многопотоковость в visual basic

В классическом научно-фантастическом романе Альфреда Бестера «Звезды — цель моя» описывается психокинетическое взрывчатое вещество ПирЕ, всего одна крупинка которого может взорвать целый дом. Для взрыва требуется совсем немного — чьего-нибудь мысленного пожелания. Герою романа приходится решать, сохранить ли тайну или же открыть ее людям, чтобы судьба мира находилась в руках и мыслях каждого человека на планете.

Нечто похожее можно сказать и о многопоточности.

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

Многопоточность в VB .NET страшит меня больше, чем все остальные новшества, причем, как и во многих новых технологиях .NET, это объясняется скорее человеческими, нежели технологическими факторами.

За несколько месяцев до презентации .NET я принимал участие в конференции VBits. Я спросил свою аудиторию, состоявшую из довольно опытных программистов Visual Basic, хотят ли они видеть свободную Многопоточность в следующей версии Visual Basic. Практически все подняли руки. Затем я спросил, кто из присутствующих знает, на что идет. На этот раз руки подняли всего несколько человек, и на их лицах были понимающие улыбки.

Я боюсь многопоточности в VB .NET, потому что программисты Visual Basic обычно не обладают опытом проектирования и отладки многопоточных приложений 1 . В реализации многопоточности для VB6 действуют повышенные меры защиты (наряду с довольно жесткими ограничениями). Существует только один путь к безопасному использованию многопоточности — вы должны хорошо разобраться в ней и правильно проектировать свои приложения.

1 Чтобы вы не думали, что я безнадежно зазнался, поясню: я программирую многопоточные приложения в течение многих лет и до сих пор сталкиваюсь с нетривиальными ошибками. Многопоточность используется во внутренней работе пакета Desaware NT Service Toolkit, и половина всего времени была потрачена на проектирование, тестирование и отладку управления программными потоками.

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

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

1 Проще говоря, я постараюсь вас до смерти напугать.

2 А если вы собираетесь пролистать эту главу, потому что не намерены использовать многопоточность, позвольте напомнить, что по умолчанию все завершители объектов работают в отдельном потоке завершения! Следовательно, проблемы многопоточности могут возникнуть и в том случае, если вы не создаете потоков в своих приложениях.

Первое знакомство с многопоточностью

Вероятно, вы как программист Visual Basic среднего или высокого уровня хотя бы в общих чертах представляете себе, что такое многопоточность. Упрощенно говоря, Windows позволяет одновременно выполнять несколько фрагментов программного кода, быстро переключаясь между ними. Но что это означает на практике?

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

Итак, любой код, работающий в системе, выполняется в программном потоке (thread).

Тогда что такое «процесс»?

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

Процесс состоит из одного или нескольких программных потоков, работающих в отдельном пространстве памяти. При запуске приложения Windows с точки зрения процесса все выглядит так, словно он один распоряжается всей системной памятью. На самом деле процесс не может записывать данные в адресное пространство других процессов, а другие процессы не могут записывать в его адресное пространство. Разделение адресных пространств обеспечивает повышенную степень защиты и является главной причиной того, что 32-разрядные версии Windows не «зависают» так часто, как прежние 16-разрядные версии 1 .

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

Все потоки многопоточного приложения работают в одном и том же пространстве памяти.

Рассмотрим следующий сценарий.

1 Ситуация также описана несколько упрощенно. В Windows NT, 2000 и ХР пространства памяти изолируются более надежно, чем в Windows 95/98/ME. Помимо разделения пространств памяти, процессы обладают и другими возможностями.

Фиаско в магазине

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

В это время миссис Купилл обнаруживает, что в web-магазине проводится распродажа 125-гигабайтных жестких дисков, о которых так мечтал мистер Купилл (чтобы наконец-то установить Office 2005). Проверив состояние кредитной карты, она видит, что у нее как раз хватает денег на покупку, и оформляет заказ.

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

Мистер Купилл с позором покидает магазин и пытается понять, что же произошло. От огорчения он не смотрит по сторонам, и его сметает толпа из пяти тысяч обезумевших подростков, спешащих в магазин за только что вышедшей приставкой Playstation 4 Х-Вох (последняя разработка MS-Sony).

Какое отношение это имеет к многопоточности, спросите вы?

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

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

Что произойдет, если эти два потока обратятся к одному объекту СОМ?

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

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

Почему же эта проблема не существовала в Visual Basic 6? Потому что VB6 создает отдельную копию всех глобальных переменных для каждого потока в многопоточном приложении или DLL 1 . Разделение осуществлялось на уровне Visual Basic 6, а не на уровне ОС. В VB .NET глобальные и общие переменные совместно используются всеми потоками приложения. Таким образом, все проблемы многопоточности лежат исключительно на вашей совести.

1 Точнее, глобальные переменные VB6 хранятся в локальной памяти потока.

Подробнее о многопоточности

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

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

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

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

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

  • счет ребенка является банковским счетом;
  • счет отца является банковским счетом.

Иначе говоря, иерархия объектов данного примера наводит на мысли о применении наследования.

Ниже приведен класс банковского счета Account приложения Threadingl. В переменной m_Account хранится текущая сумма на счету. Кроме того, в переменных объекта хранятся суммарный расход и суммарные поступления за время существования счета. Также в классе Account хранится объект типа Random — объект CLR, предназначенный для работы со случайными числами и заменяющий функцию VB6 Rnd. Функция GetRandomAmount создает значение в интервале от 0 до 1 доллара и имитирует расходуемые суммы.

Protected m_Account As Double

Protected m_Spent As Double

Protected m_Deposited As Double

Private Shared m_Random As New Random()

‘ Возвращает случайную сумму от $0 до $1.00.

Protected Shared Function GetRandomAmount() As Double

Dim amount As Double

amount = Int(m_Random.NextDouble * 100)

GetRandomAmount = amount / 100

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

Листинг 7.1. Свойства класса Account 1

Set(ByVal Value As Double)

Property Balance() As Double

Set(ByVal Value As Double)

Property Deposited() As Double

Set(ByVal Value As Double)

1 Все исходные тексты можно найти на сайте издательства «Питер» www.piter.com. — Примеч. ред.

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

Метод Withdraw (листинг 7.2) получает в качестве параметра сумму, снимаемую со счета, и возвращает фактически снятую сумму (которая при нехватке денег на счету может быть меньше запрашиваемой). Кроме того, снятая сумма прибавляется к суммарному расходу (m_Spent ). Метод Deposit заносит указанную сумму на текущий счет и прибавляет ее к суммарным поступлениям (m_Deposi ted). Метод Clear сбрасывает переменные класса в исходное состояние.

Листинг 7.2. Методы Withdraw и Depost класса Account

‘ Попытаться снять со счета запрашиваемую сумму,

‘ вернуть фактически снятую сумму.

Protected Function Withdraw(ByVal amount As Double) As Double

If amount > m_Account Then

m_Account = m_Afcount — amount

m_Spent = m_5pent + amount

Protected Sub Deposit(ByVal amount As Double)

m_Account = m_Account + amount

m_Deposited = m_Deposfted + amount

PubVic Overridable Sub Clear()

Счет ребенка представлен классом KidAccount , производным от класса Account . В этом классе добавлена переменная m_FalledRequests , в которой хранится количество случаев, когда ребенок хотел потратить деньги, но не располагал нужной суммой на счету. Как нетрудно предположить, эта переменная будет быстро увеличиваться.

Для зачисления денег на счет используется метод GetAllowance . Можно ли было воспользоваться методом Deposit ? Напрямую — нельзя. Поскольку метод Deposit базового класса является защищенным, он не может напрямую вызываться извне. Мы также могли создать новый метод Deposit и воспользоваться ключевым словом Shadows , чтобы скрыть унаследованный метод базового класса (а также вызвать метод базового класса через объект MyBase , но об этом речь пойдет в главе 10).

Private m_FailedRequests As Double

Readonly Property FailedRequests() As Double

‘ Получение карманных денег от родителя

Public Sub GetAllowance(ByVal amount As Double)

Деньги расходуются методом Spend . Метод выбирает случайную сумму до $1 и пытается снять ее со счета. Если при снятии баланс падает до нуля, попытка считается неудачной, а переменная m_FailedRequests увеличивается. Метод Clear сбрасывает как переменную m_FailedRequests , так и методы базового класса (листинг 7.3).

Листинг 7.3. Методы Spend и Clear класса KidAccount

‘ Попытка потратить случайную сумму

Public Sub Spend()

Dim amount As Double

If amount > m_Account Then amount = m_Account

If amount = 0 Then m_FailedRequests = m_FailedRequests + 1

Else Withdraw (amount)

‘ Обнуление переменных объекта и базового класса

Overrides Sub Clear()

Класс ParentAccount (листинг 7.4), также объявленный производным от Account , моделирует родительский счет. Метод GiveAllowance выбирает случайную сумму, которая снимается со счета отца. Возвращаемое значение определяет сумму, фактически зачисляемую на счет ребенка методом GetAllowance . Метод Deposi tPayroll зачисляет на родительский счет ежемесячную зарплату.

Листинг 7.4. Класс ParentAccount

‘ Метод выбирает случайную сумму карманных расходов

‘ и снимает ее со счета.

Public Function GiveAllowanceO As Double

Dim amount As Double

1 Возвращение фактически снятой суммы (может быть равна 0)

Public Sub DepositPayrolKByVal amount As Double)

Класс FamilyOperation имитирует финансовые отношения в семье. В него включена переменная Kids (), содержащая объекты KidAccount , по одному для каждого ребенка в семье. Переменная Parent содержит объект ParentAccount , представляющий родительский счет.

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

Для управления потоками используется переменная Threads , содержащая ссылку на массив объектов System.Threading.Thread — эти объекты управления потоками поддерживаются CLR.

Переменная m_NumberOfKids содержит количество детских счетов. Присваивая переменной m_Stopping значение True, вы сигнализируете фоновым потокам о необходимости завершения. Переменная m_Random относится к типу Random () и используется имитатором для определения суммы, переводимой на счета детей.

Private Kids() As KidAccount

Private Parent As ParentAccount

Private ThreadsO As System.Threading.Thread

Private m_NumberOfKids As Integer

Private m_Stopping As Boolean

Private m_Random As New Random()

Объект FamilyOperation спроектирован таким образом, что значение свойства NumberOfKids задается перед началом имитации, причем изменить его уже не удастся. Метод Get тривиален: он просто возвращает значение внутренней переменной m_NumberOfKids . Метод Set сначала проверяет присваиваемую величину; допустимыми считаются значения в интервале от 1 до 50. Кроме того, метод проверяет, было ли значение m_NumberOfKids присвоено ранее; если проверка дает положительный результат, инициируется ошибка (листинг 7.5).

Метод Throw является новым и рекомендуемым способом инициирования ошибок в VB .NET. В CLR определено довольно большое количество документированных исключений. В нашем примере используются исключения ArgumentOutOfRangeException (недопустимое значение параметра или свойства) и InvalidOperationException (попытка выполнения недопустимой операции). Исключения и новый механизм обработки ошибок рассматриваются в главе 8.

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

Листинг 7.5. Свойство NumberOfKids класса FamilyOperation

Property NumberOfKidsO As Integer

SeUByVal Value As Integer)

Then Throw New ArgumentOutOfRangeException(_

«Property must be between 1 and 50»)

If m_NumberOfKids <> 0 Then

Throw New InvalidOperationException(_

«NumberOfKids may only be set once»)

Dim Kid As Integer

ReDim Kids(m_NumberOfKids — 1)

Parent = New ParentAccount()

Метод KillFamily 1 просто присваивает переменной m_Stopping значение True , сигнализируя потокам о завершении работы. О том, как это происходит, будет рассказано ниже. Средства зачисляются на родительский счет методом ParentPayday .

1 При написании этого метода ни одна семья не пострадала.

Public Sub ParentPayday(ByVal Amount As Double)

Свойства TotalDepositToParent, TotalAllocatedByParent и ParentBalance предназначены для получения статистики о родительском счете.

Всегда должно соблюдаться следующее условие:

TotalDepositToParent — TotalAllocatedByParent = ParentBalance

Код этих свойств приведен в листинге 7.6.

Листинг 7.6. Свойства TotaiDepositedToParent, TotalAllocatedByParent и ParentBalance

Public Readonly Property TotalDeposi tedToParentO As Double

Return 0 Return Parent.Deposited

Public Readonly Property TotalAllocatedByParentO As Double

Return 0 Return Parent.Withdrawn

Public Readonly Property ParentBalance() As Double

Свойства TotalGivenToKids, TotalSpentByKids, TotalKidsBalances и Total FailedRequests используются для получения сводной статистики по всем детским счетам. Следующее условие всегда должно выполняться:

Программный код этих свойств приведен в листинге 7.7.

Листинг 7.7. Свойства для получения сводной информации о счетах детей

Public Readonly Property TotalGivenToKidsO As Double

Dim Idx As Integer

Dim Total As Double

Total = Total + Kids(Idx).Deposited

Public Readonly Property TotalSpentByKidsQ As Double

Dim Idx As Integer

Dim Total As Double

Total = Total + Kids(Idx).Withdrawn

Public Readonly Property TotalKidsBalancesO As Double

Dim Idx As Integer

Dim Total As Double

Total = Total + Kids(Idx).Balance

Public Readonly Property TotalFailedRequestsO As Double

Dim Idx As Integer

Dim Total As Double For >

Total = Total + Kids(Idx).FailedRequests

Вся настоящая работа выполняется методом KidsSpending . Он состоит из цикла, выполняемого до тех пор, пока переменной m_Stopping не будет присвоено значение True (листинг 7.8). В цикле последовательно выполняются следующие действия:

  • выбор случайного ребенка;
  • запрос «карманных денег» с родительского счета — случайной суммы не более $1;
  • зачисление возвращаемой суммы на счет ребенка;
  • ребенку предоставляется возможность истратить случайную сумму денег.

Листинг 7.8. Функция KidsSpending

Dim Childlndex As Integer

Dim Allowance As Double

Dim thiskid As KidAccount

‘ Случайно выбранный ребенок тратит некоторую сумму.

Childlndex = CInt(Int(m_Random.NextDouble() * СDbl(m_NumberOfKids)))

Loop Until m_Stopping

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

Метод StartThread создает заданное количество новых потоков и запускает их. Он динамически переобъявляет размер массива Threads и создает новые объекты Thread , каждому из которых передается делегат для метода KidsSpending .

Вероятно, вы знакомы с оператором VB6 AddressOf , возвращающим адрес (указатель) функции в модуле. В VB .NET оператор AddressOf возвращает делегата, которого лучше всего представлять себе как указатель на метод конкретного объекта. В данном примере возвращается указатель на метод KidsSpending текущего объекта 1 .

1 Напрашивается вопрос: можно ли получить делегата для метода другого объекта и вызвать его? Ответ: да, можно. Более того, как будет показано ниже, именно так работают события VB.NET.

Каждый запущенный поток вызывает метод KidsSpending .

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

Задавая свойству IsBackground значение True , мы сообщаем CLR, что создаваемый поток является фоновым и должен уничтожаться автоматически при прекращении работы всех не фоновых потоков 1 .

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

Public Sub StartThreads(ByVal ThreadCount As Integer)

ReDim Threads(ThreadCount — 1)

В методе StopThreads продемонстрирован новый механизм обработки ошибок в VB .NET. Базовый принцип уничтожения потоков прост. Сначала вызывается метод KillFamily , который присваивает переменной m_Stopping значение True . После этого все потоки должны завершиться при достижении конца цикла Do . Метод Join объекта Thread заставляет текущий поток дождаться фактического завершения заданного потока.

В приведенном примере метод Join всегда работает нормально, поскольку все потоки начинают работу при создании. Но если метод будет вызван перед инициализацией массива Threads , метод GetUpperBounds инициирует исключение. В результате управление передается блоку, следующему сразу же за командой Catch . В листинге 7.9 ошибки просто игнорируются.

Листинг 7.9. Метод StopThreads

Dim Idx As Integer Try

KillFamily() ‘ Все потоки должны остановиться

‘ Дождаться завершения потока

Threads(Idx).Join() Next Catch

‘ Игнорировать все ошибки

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

На форме находятся два текстовых поля, в которых вводится количество банковских счетов у детей и количество потоков. Также на ней имеются три кнопки: для занесения денег на счет, для запуска и для остановки имитации. Результаты выводятся в виде списка. С формой ассоциируется единственная переменная ByFamily , содержащая ссылку на объект FamilyOperation . Перед выгрузкой формы вызывается метод StopTnreads , поэтому фоновые потоки останавливаются даже в том случае, если пользователь не нажал кнопку Stop .

Dim myFamily As FamilyOperation

1 Форма переопределяет Dispose для очистки списка компонентов.

Public Overloads Overrides Sub Dispose()

MyBase. Dispose() If Not (components Is Nothing) Then

‘ Остановить потоки If Not myFamily Is Nothing Then

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

Листинг 7.10. Формы метода UpdateResults

IstResults.Items.Add («- Total Deposited: » +

IstResults.Items.Add («- Total Withdrawn: » +

IstResults.Items.Add («- Expected Balance: » + _

IstResults.Items.Add («- Actual Balance: » + _

IstResults.Items.Add («- Total Deposited: » + _

IstResults.Items.Add («- Total Withdrawn: » + _

IstResults.Items.Add («- Expected Balance: » +_

IstResults.Items.Add («- Actual Balance: » +

Содержимое списка обновляется раз в одну-две секунды по событиям таймера. Кнопка Deposit заносит заданную сумму на родительский счет при помощи метода FamilyOperation.ParentPayday. Кнопка Start создает новый объект FamilyOperation , задает значение свойства NurtiberOfKids и затем вызывает метод StartThreads с указанным количеством потоков. Метод StopThreads сначала останавливает потоки, а затем выводит окончательный результат в списке. Программный код, выполняющий все эти операции, приведен в листинге 7.11.

Листинг 7.11. Код формы приложения Threading 1

Protected Sub Timerl_Tick(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles timerl.Tick

Protected Sub cmdDeposit_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles cmdDeposit.Click

Dim Amount As Double

Amount = VaHtxtDepositO .Text)

Protected Sub cmdStart_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles cmdStart.Click

myFamily = New FamilyOperation()

Dim Kids As Integer

Dim Threads As Integer

Protected Sub cmdStop_Click(ByVal sender As System.Object, _

ByVal e As System.EventArgs) Handles cmdStop.Click

Чтобы поближе познакомиться с работой нашего приложения, попробуем запустить его со стандартными параметрами: для десяти детей и одного потока. Нажмите кнопку Deposit 20 или 30 раз, чтобы почувствовать, как деньги переходят от отца к детям и как дети их расходуют. Типичная ситуация показана на рис. 7.1.

Рис. 7.1. Форма приложения с одним потоком

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

Вам удалось найти какие-нибудь недочеты?

Попробуйте запустить приложение для 10 детей с 10 фоновыми потоками. Нажмите кнопку Deposit несколько раз (чтобы получить результат, показанный на рис. 7.2, потребуется несколько сот нажатий).

Рис. 7.2. Форма приложения с несколькими потоками

Тестируемое приложение устроено чрезвычайно просто. В объекте Account определены два метода:

Protected Function Withdraw(ByVal amount As Double) As .Double

If amount > m_Account Then amount = m_Account

m_Account = m_Account — amount ,

m_Spent = m_Spent + amount

Protected Sub Deposit(ByVal amount As Double)

m_Account = m_Account + amount

m_Deposited = m_Deposited + amount

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

Однако в нашем примере это не так.

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

m_Spent = m_Spent + amount

m_Account = m_Account + amount

m_Deposited = m_Deposited + amount

Как это возможно?

Давайте еще раз проанализируем метод Deposit , но на этот раз воспользуемся листингом на промежуточном языке (IL), сгенерированном для этого метода (листинг 7.12 1 )

Листинг 7.12. Промежуточный код метода Deposit объекта Account

.meth’od family instance void Deposit(float64 amount) 11 managed

// Code size 31 (Gxlf)

IL_0003: Idfld float64 Threading.Account::m_Account

IL_000a: stfld float64 Threading.Account::m_Account

IL_0011: Idfld float64 Threading.Account::m_Deposited

IL_0018: stfld float64 Threading.Account::m_Deposited

IL_001e: ret > // end of method Account::Deposit

1 Листинг получен при помощи дизассемблера .NET. Проблемы, связанные с относительной простотой дизассемблирования приложений .NET (VB или С#), рассматриваются в главе 16.

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

преобразуется в фрагмент

IL_0003: Idfld float64 Threading.Account::m_Account

IL_000a: stfld float64 Threading.Account::m_Account

Как можно предположить, в начале этого фрагмента m_Account дважды заносится в стек, после чего верхняя величина в стеке загружается в регистр. Затем загружается указатель на аргумент amount и две величины суммируются. Результат присваивается переменной m_Account , находящейся в стеке (поскольку она была загружен дважды). Вероятно, происходящее будет понятно читателям, имеющим опыт работы с обратной польской записью 1 (например, владельцам инженерных калькуляторов HP). Остальные пусть не огорчаются; не так уж важно, что именно здесь происходит. Вам лишь необходимо понять, что действия выполняются в следующей последовательности.

1 Информацию об обратной польской записи можно найти по адресу http://www.hpmuseum.org/rpn.htm.

1. Переменная m_Account загружается в регистр.

2. Параметр amount прибавляется к значению m_Account .

3. Результат сохраняется в переменной m_Account .

Что произойдет, если два потока попытаются одновременно выполнить одну и ту же операцию? Например, возможна следующая последовательность событий.

1. Поток 1 загружает m_Account в регистр.

2. Операционная система прерывает поток 1.

3. Поток 2 загружает m_Account в регистр.

4. Поток 2 прибавляет параметр amount к значению в регистре.

5. Поток 2 сохраняет результат в переменной m_Account .

6. Поток 2 прерывается.

7. Поток 1 прибавляет параметр amount к содержимому регистра (в котором восстанавливается значение, содержавшееся до прерывания потока на шаге 2, однако текущее содержимое регистра не учитывает изменения, внесенные потоком 2!).

8. Поток 1 сохраняет результат сложения в переменной m_Account , фактически стирая значение, сохраненное потоком 2.

Результат — значение amount прибавлялось к переменной m_Account дважды, но в переменной отражена лишь одна из этих операций!

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

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

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

Как вы помните, в нашем примере переводимые суммы составляли от 0 до 1 доллара. Если предположить, что средняя сумма равна 50 центам, а общие зачисления на счет равны 7 миллионам долларов (см. рис. 7.2), значит, вероятность ошибки равна примерно 1/14 000 000.

Спрашивается, как отлаживать код, в котором ошибка возникает один раз из 14 000 000 выполнений? А ведь последствия ошибок могут быть самыми разными: от пропажи нескольких центов со счета в банковской программе до смерти пациента в больнице (при выборке сведений о дозировках лекарств из медицинской базы данных).

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

Означает ли это, что вы не должны использовать многопоточность?

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

А если вы еще недостаточно напуганы, учтите еще одно обстоятельство.

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

Первый уровень защиты: проектирование

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

Существует несколько разных подходов к устранению этих проблем.

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

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

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

Каждый поток должен сохранить свой индекс. Для сохранения можно было бы воспользоваться стековой переменной (например, одной из переменных метода KidsSpending ), однако в приложении Threading2 продемонстрирован другой подход. Переменная ThisThreadlndex объявляется общей для всех экземпляров класса, что эквивалентно ее объявлению как глобальной. Но еще важнее тот факт, что с этой переменной ассоциируется атрибут ThreadStatic (), означающий, что для каждого потока приложения создается отдельная копия этой переменной 1 . Вам это ничего не напоминает? Вспомните, о чем говорилось выше: именно так VB6 поступает со всеми глобальными переменными. Для поддержки хранения отдельных копий переменных для каждого потока операционная система использует локальную память потока (как при указании атрибута ThreadStatiс , так и при хранении глобальных переменных в VB6).

Поскольку каждый поток создает и выполняет свой метод KidsSpending , копия переменной Threadlndex загружается текущим значением счетчика Thread-Counter, который после этого увеличивается.

Впрочем, на самом деле индекс потока сохраняется несколько иначе: сначала переменная ThreadCounter увеличивается, а затем ThisThreadlndex присваивается новое значение, уменьшенное на 1. Существует очень малая вероятность того, что сама переменная ThreadCounter приведет к проблемам многопоточности, поскольку она совместно используется всеми потоками. Чтобы ликвидировать даже малейшую вероятность конфликта между потоками за эту переменную (например, получения одинаковых индексов двумя потоками), для увеличения переменной ThreadCounter применяется метод Threading. Interlocked. Increment . Этот общий метод 2 класса Threading. Interlocked выполняет атомарное увеличение переменной. Другими словами, процесс увеличения не может быть прерван операционной системой 3 .

1 Как будет показано в главе 11, атрибуты .NET — весьма обширная тема. Пока можете рассматривать атрибуты как способ передачи данных CLR — в нашем примере это информация о том, как должна компилироваться переменная.

2 Напоминаю: общие методы принадлежат всему классу, а не его конкретным объектам и могут вызываться без указания объекта.

Илон Маск рекомендует:  Что такое код msession_unlock

3 Возможно, вы подумали, нельзя ли воспользоваться методом Interlocked. Increment для увеличения переменной m_Account класса Account? Нет, нельзя (метод позволяет увеличивать переменные только на 1), но вы мыслите в верном направлении, и позднее я покажу один из вариантов применения этой методики.

Переменная ThisThreadlndex содержит уникальный номер потока, который используется при индексации массива Kids (листинг 7.13) и гарантирует, что с каждым детским счетом работает один и только один поток.

Листинг 7.13. Метод KidsSpending приложения Threading ?

Private ThreadCounter As Integer

Private Shared ThisThreadlndex As Integer

Public Sub KidsSpending()

Dim Allowance As Double

Dim thiskid As KidAccount

ThisThreadlndex = Threading.Interlocked.Increment(ThreadCounter) — 1

‘ Каждый детский счет обслуживается одним потоком

thiskid. SpendO Loop Until m_Stopping

Результат показан на рис. 7.3. После многочисленных нажатий кнопки Deposit становится видно, что родительский объект по-прежнему подвержен многопоточным ошибкам, потому что он совместно используется разными потоками. Тем не менее объекты Kids работают правильно: в результате изменений в архитектуре приложения они перестали быть общими.

Рис. 7.3. Приложение Threading

Почему я представил вам пример, ограничивающийся частичным решением проблемы?

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

Из-за применения механизма подсчета ссылок объекты СОМ также подвержены многопоточным проблемам: внутренние счетчики ссылок являются общими для всех потоков, использующих объект. В СОМ определяются три разных потоковых модели: однопоточная ( single thread ), совместная модель ( STA, Single Threaded Apartment ) и свободная ( МТА, Multi-Threaded Apartment ). Решения для однопоточной и совместной модели основаны на описанных выше принципах. Ограничивая доступ к объекту потоком, создавшим объект, эти модели фактически ликвидируют проблемы многопоточности, поскольку с заданным объектом (и всеми его методами/свойствами) может работать только один поток.

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

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

Второй уровень защиты: синхронизация

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

В проекте Threading3 продемонстрирован подход, основанный на «грубой силе», при котором синхронизируется весь класс. Это означает, что со всеми методами и свойствами класса потоки могут работать лишь поочередно. Если некоторый поток работает с методом или свойством класса, никакие другие потоки не могут работать с методами или свойствами этого класса. CLR переводит их в состояние ожидания до тех пор, пока первый поток не завершит операции с методом или свойством. Данное приложение основано на приложении Threading 1 (другими словами, потоки выбирают детские счета случайным образом).

Фрагменты, приведенные в листинге 7.14, дают представление об изменениях в классах Account и KidsAccount .

Листинг 7.14. Изменения в классах Account и KidsAccount в приложении Threading3

‘ Наследует от ContextBoundObject

‘ для синхронизации KidAccount.

‘ чтобы избавиться от проблем с многопоточным доступом.

Прежде всего бросается в глаза то, что класс Account объявляется производным от ContextBoundObject. В первоначальном варианте он был производным только от Object (поскольку в .NET все типы являются производными от Object ). Класс, производный от ContextBoundObject (который, в свою очередь, является производным от MarshalByRefObject и Object ), наследует дополнительную функциональность, позволяющую CLR ассоциировать объекты с контекстом. В частности, контекст позволяет управлять внешним доступом. Объявление класса производным от ContextBoundObject не влияет на внутреннее устройство самого класса Account . Однако при работе с классом KidAccount CLR видит установленный атрибут Synchronization , указывающий на то, что одновременный доступ к классу должен ограничиваться одним потоком.

На рис. 7.4 показан результат выполнения примера Threading3 после многих нажатий кнопки Deposit .

Рис. 7.4. Приложение Threading3

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

Мы уже выяснили, что в данном случае синхронизация критична только для двух методов: Deposit и Withdraw . Следовательно, синхронизация всех методов и свойств класса — явный перебор, замедляющий работу программы.

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

Помимо атрибута Synchronization классу можно назначить атрибут Thread-Af inity . Атрибут Synchronization сообщает CLR о том, что с объектом в любой момент времени может работать только один поток. Атрибут ThreadAffinity сообщает CLR, что объект доступен только для того потока, которым он был создан, и в случае необходимости CLR следует организовать передачу данных между потоками, чтобы один поток мог получить доступ к методам и свойствам объекта, существующего в контексте другого потока. Аналогичная концепция используется СОМ для реализации потоковой модели STA. Учтите, что эти атрибуты не обеспечивают синхронизации общих методов и свойств, относящихся ко всем экземплярам класса.

Атрибуты Synchronization и ThreadAffinity обеспечивают простейшую возможность синхронизации доступа ко всем методам и свойствам класса. Как оценить разумность этого подхода? Очень просто: заглянуть в бета- документацию, где под заголовком «Принципы синхронизации» сказано: «. Годится для наивных пользователей».

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

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

В программе Threading4 продемонстрирован один из способов решения этой задачи с применением новой команды VB .NET Sync Lock . Во внутренней реализации этой команды синхронизация программных блоков осуществляется при помощи специального объекта, называемого монитором. В листинге 7.15 приведены изменения исходной программы Threadingl.

Листинг 7.15. Измененные фрагменты приложения Threading4 (относительно Threadingl)

Protected m_Account As Double

Protected m_Spent As Double

Protected m_Deposited As Double

Private Shared m_Random As New Random()

Protected LockingObject As String = «HoldTheLock»

‘ Попытаться снять со счета запрашиваемую сумму,

‘ вернуть фактически снятую сумму.

Protected Function Withdraw(ByVal amount As Double)

As Double SyncUock LockingObject

If amount > m_Account Then

m_Account = m_Account — amount

m_Spent = m_Spent + amount

Protected Sub Deposit(ByVal amount As Double)

m_Account = m_Account + amount

m_Deposited = m_Deposited + amount

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

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


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

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

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

Допустим, у вас имеется два потока, А и В.

Поток А входит в блок SyncLock и блокирует переменную LockingObject .

Поток В пытается войти в блок SyncLock , но ему это не удается из-за блокировки переменной LockingObject .

Рис. 7.5. Приложение Threading4

Тем временем поток А, все еще находящийся в блоке Sync Lock, переходит в ожидание некоторой операции, выполняемой потоком В 1 .

Поток А ожидает действий со стороны потока В, но работа потока В приостановлена до тех пор, пока объект А не снимет блокировку с объекта LockingObject. В результате оба потока стоят на месте.

Подобная ситуация называется взаимной блокировкой ( deadlock ). Она возникает каждый раз, когда работа двух или более потоков приостанавливается в ожидании действий со стороны других потоков 2 .

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

1 Существуют разные ситуации, при которых потоку приходится ожидать выполнения операций другим потоком. Может быть, поток В отвечает за некоторую фоновую операцию или содержит класс с установленным атрибутом ThreadAf f i ni ty, обращения к которому должны происходить из этого потока.

2 Один из принципов, известных многим программистам, гласит: «Если взаимная блокировка может произойти, она непременно произойдет».

Синхронизация и ожидание

При внимательном взгляде на рис. 7.5 можно заметить в конце списка новую строку. В ней выводится количество неудачных попыток — сумма переменных m_FailedRequests по всем объектам детских счетов. Другими словами, значение в этой строке увеличивается каждый раз, когда ребенок хочет потратить деньги, но оказывается, что у него на счету нет достаточной суммы 1 .

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

Листинг 7.16. Метод KidsSpending (приложение Threading4)

Dim Childlndex As Integer

Dim Allowance As Double

Dim thiskid As KidAccount

‘ Случайно выбранный ребенок тратит некоторую сумму.

Childlndex = CInt(Int(m_Random.NextDouble() * CDbl(m_NumberOfKids)))

thiskid.Spend() Loop Until m_Stopping

Чтобы ознакомиться с побочными эффектами такого решения, откройте окно диспетчера задач во время работы этой программы.

Рис. 7.6. Влияние приложения Threading4 на быстродействие системы

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

Если вы увидите, что загрузка процессора составляет 99 % (см. рис. 7.6), значит, что-то определенно не так. И действительно, наша программа поглощает все свободное процессорное время и существенно снижает быстродействие системы. Короче говоря, это приложение спроектировано просто ужасно.

На самом деле наше приложение должно быть достаточно «умным» и дожидаться появления денег на счету ребенка перед тем, как их тратить. Одна из приятных особенностей многопоточных приложений заключается в том, что вы можете приостанавливать отдельные потоки без остановки всей программы. Например, можно приостановить поток, выполняющий фоновую операцию, и это никак не отразится на пользовательском интерфейсе. Более того, приостановленный поток в режиме ожидания (например, ожидающий объекта, заблокированного вызовом Sync Lock ) практически не расходует системных ресурсов.

Проблема решается в приложении Threading5. Начнем с рассмотрения листинга 7.17, в котором для синхронизации доступа вместо блока SyncLock используется объект Mutex .

Листинг 7.17. Класс Account приложения Threading5

As Double Protected m_Spent As Double

Protected m_Deposited As Double

Private Shared m_Random As New Random()

Protected myMutex As New Threading.Mutex(False)

Protected Shared MoneyAvailable As New Threading.ManualResetEvent(False)

Property Deposited() As Double

‘ Попытаться снять со счета запрашиваемую сумму,

‘ вернуть фактически снятую сумму.

Protected Function Withdraw(ByVal amount As Double) As Double

Catch e As Threading.ThreadlnterruptedException

If amount > m_Account Then

m_Account = m_Account — amount

m_Spent = m_Spent + amount

Protected Sub Deposit(ByVal amount As Double)

myMutex.WaitOne() Catch e As Threading.ThreadlnterruptedException

m_Account = ra_Account + amount

m_Deposited = m_Deposited + amount

Чем же применение объекта Mutex принципиально отличается от решения с SyncLock ? B данном случае — ничем. В некоторых ситуациях объект Mutex обеспечивает большую гибкость, поскольку ожидание может выполняться сразу для нескольких объектов Mutex . Я привел этот пример только для того, чтобы показать, что существуют и другие объекты синхронизации. Обработчик ошибок предотвращает ошибку времени выполнения при прерывании потока (как вы вскоре убедитесь, это существенно). Прежде чем переходить к написанию многопоточных приложений, непременно прочитайте справочную документацию пространства имен System.Threading и познакомьтесь с разными объектами синхронизации 1 .

В классе Account также определяется общий объект ManualResetEvent , использующий события синхронизации Win32 (термин «event» не имеет ничего общего с привычными событиями Visual Basic). Объект объявлен общим, поскольку признак наличия денег в нашей имитации является общим для всех объектов детских счетов 2 :

Protected Shared MoneyAvailable As New Threading.ManualResetEvent(False)

Метод Spend класса KidAccount проверяет, равна ли доступная сумма нулю. При отсутствии денег вызывается метод MoneyAvailable.WaitOne() , который приостанавливает выполнение потока и ожидает установки объекта ManualResetEvent другим потоком.

Команда Wait находится внутри блока Try . Это связано с тем, что в некоторых ситуациях ожидание прерывается самим приложением, точнее говоря, в конце своей работы приложение должно прервать все ожидающие потоки, чтобы обеспечить корректное завершение 3 . В листинге 7.18 показано, как мьютекс MoneyAvailable используется классом KidAccount.

1 Почему я не рассматриваю их в книге? Потому что моя цель — представить основные концепции, лежащие в основе многопоточности, и помочь вам научиться программировать многопоточные приложения в VB.NET с приемлемым уровнем надежности. От пересказа сведений, содержащихся в документации, никакого проку не будет. Небольшой совет: не ограничивайтесь описаниями объектов. Загляните в документацию Win32 Platform SDK и ознакомьтесь с синхронизационными функциями API; это даст вам более глубокое представление о работе различных объектов.

2 В более строгой иерархии объект ManualResetEvent следовало бы ассоциировать с объектом Pa rent, а объекты Kid Ac count — с конкретным объектом Pa rent Account. В нашем простом примере это несущественно, однако такие обстоятельства должны учитываться при проектировании иерархий классов, предназначенных для повторного использования.

3 В нашем приложении используются потоки со свойством IsBackground = True, поэтому при прекращении работы основного потока приложение будет успешно завершено. С другой стороны, хороший стиль программирования требует останавливать все потоки перед выходом из приложения, не полагаясь на капризы CLR.

Листинг 7.18. Класс KidAccount приложения Threading5

‘ чтобы избавиться от проблем с многопоточным доступом.

Private m_FailedRequests As Double

Readonly Property Fai ledRequests() As Double

End Get End Property

‘ Получить карманные деньги от родителя

Public Sub GetAllowance(ByVal amount As Double)

‘ Попытаться потратить случайную сумму

Public Sub SpendO Dim amount As Double

‘ Дождаться поступления денег Try

If m_Account = 0 Then MoneyAvailable.Wai tOne()

‘ Ожидание прерывается при выходе из приложения.

If amount > m_Account Then amount = m_Account

If amount = 0 Then

m_FailedRequests = m_FailedRequests + 1

‘ Обнуление переменных объекта и базового класса Overrides

Sub Clear() m_FailedRequests = 0

Объект MoneyAvailable класса ManualResetEvent находится под управлением родительского счета. Если при попытке перевода денег на детские счета выясняется, что текущий баланс равен 0, объект MoneyAvailable сбрасывается. Когда объект ManualResetEvent находится в установленном состоянии, все потоки, ожидающие этого объекта, могут продолжать работу. Когда объект сбрасывается, как в листинге 7.19, все потоки, ожидающие объекта ManualResetEvent , приостанавливаются до его установки. Метод DepositPayroll устанавливает объект ManualResetEvent , тем самым оповещая ожидающие потоки о наличии денег на счету.

Листинг 7.19. Класс ParentAccount приложения Threading5

‘ Метод выбирает случайную сумму карманных расходов

1 и снимает ее со счета.

Public Function GiveAllowance() As Double

Dim amount As Double

‘ Вернуть фактически снятую сумму (может быть равна 0).

‘ Если денег не осталось, остановить процесс.

‘ Внимание: здесь присутствует

‘ нетривиальная ошибка синхронизации.

‘ Удастся ли вам ее найти?

If m_Account = 0 Then MoneyAvailable.Reset()

Return (amount) End Function

Public Sub DepositPayroll(ByVal amount As Double)

‘ Установить объект ManualResetEvent —

‘ сообщить детям о наличии денег.

Для корректного завершения приложения также необходимо модифицировать метод StopThreads . При обнаружении ожидающего потока (листинг 7.20) ожидание прерывается методом Threads. Interrupt . В результате операция ожидания инициирует исключение, которое в нашем примере перехватывается и игнорируется.

Листинг 7.20. Метод StopThreads приложения Threading5

Dim Idx As Integer Try

‘ Остановить все потоки

‘ Ожидать завершения потока

‘ Теоретически не исключена редкая ошибка синхронизации —

‘ что, если поток перейдет в состояние ожидания

‘ после этого сравнения?

If (Threads(Idx).ThreadState And

System.Threading.ThreadState.WaitSleepJoin) <> 0 Then

‘ Игнорировать все ошибки

Результат показан на рис. 7.7.

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

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

Рис. 7.7. Приложение Threading5

Некоторые тонкости синхронизации

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

Какой поток остановлен?

Пример Threading5 построен на основе примера Threadingl. В листинге 7.21 приведен основной код метода ThreadingS.

Листинг 7.21. Метод KidsSpending приложения Threading5

Dim Childlndex As Integer

Dim Allowance As Double

Dim thiskid As KidAccount

‘ Случайно выбранный ребенок тратит некоторую сумму.

Childlndex = CInt(Int(m_Random.NextDouble() * _

if (Not m_Stopping) Then thiskid.GetAllowance (Allowance)

Loop Until m_Stopping

Одно из изменений — проверка флага m_Stopping перед вызовом GetAllowance . Необходимость проверки вызвана тем, что существует вероятность выхода из функции GiveAllowance из-за прерывания потока. В этом случае безусловный вызов GetAllowance приведет к повторному блокированию потока, которое уже не прервется. Таким образом, возникает потенциальная ситуация взаимной блокировки, поскольку главный поток будет бесконечно ждать завершения потока.

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

Как справиться с этой проблемой?

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

Неожиданности с общими переменными

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

Листинг 7.22. Методы GiveAllowance и DepositPayroll приложения Threading5 (повторение)

Public Function GiveAllowance() As Double

Dim amount As Double

‘ Вернуть фактически снятую сумму (может быть равна 0).

‘ Если денег не осталось, остановить процесс.

‘ Внимание: здесь присутствует

‘ нетривиальная ошибка синхронизации.

‘ Удастся ли вам ее найти?

If m_Account = 0 Then MoneyAvailable.Reset()

Public Sub DepositPayroll(ByVal amount As Double)

‘ Установить объект ManualResetEvent —

‘ сообщить детям о наличии денег.

Рассмотрим следующую последовательность событий.

  • Поток вызывает метод DepositPayroll.
  • Второй поток продолжает работать. (Даже если родительский счет опустел, другие потоки продолжают работать в течение некоторого промежутка времени, поскольку у детей еще есть деньги.) Этот поток вызывает метод GiveAllowance.
  • Второй поток прерывается непосредственно после проверки условия m_Account = 0.
  • Первый поток продолжает выполнять метод Deposi tPayroll, вызывает метод Deposit и устанавливает объект MoneyAvailable класса ManualResetEvent.
  • Второй поток продолжает работу и сбрасывает объект MoneyAvai lable.

Результат — излишнее ограничение доступа к объектам детских счетов.

Вероятность такого совпадения очень мала — настолько, что в приложении вы практически никогда ее не встретите (даже если вам каким-то образом удастся ее обнаружить). Но теоретически это все же возможно.

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

Потому что сам объект MoneyAvailable класса ManualResetEvent является общей переменной, к которой могут одновременно обращаться несколько потоков!

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

В нашем примере эта потенциальная проблема решается включением проверки условия в метод GiveAllowance и оба вызова DepositPayroll в блоке SyncLock (с той же переменной).

Окончание работы приложения

Давайте вернемся к новому варианту функции StopThreads (листинг 7.23).

Листинг 7.23. Функция StopThreads приложения Threading5

Dim Idx As Integer

KillFamily() ‘ Остановить все потоки

‘ Ожидать завершения потока.

‘ Теоретически не исключена редкая ошибка синхронизации —

‘ что, если поток перейдет в состояние ожидания

‘ после этого сравнения?

If (Threads(Idx).ThreadState And

System.Threading.ThreadState.WaitSleepJoin) <> 0 Then

‘ Игнорировать все ошибки

Также рассмотрим функцию Spend :

Dim amount As Double

‘ Дождаться поступления денег.

If m_Account = 0 Then MoneyAvailable .WaitOne()

‘ Ожидание прерывается при выходе из припожения.

Рассмотрим следующую ситуацию.

  • Метод StopThreads вызывается в тот момент, когда поток выполняет метод Spend , а переменная m_Account равна нулю (у ребенка кончились деньги). У родителя тоже нет денег, поэтому объект MoneyAvailable сбрасывается.
  • Система переключается с этого потока на другой, в котором выполняется StopThreads .
  • Метод StopThreads проверяет условие, обнаруживает, что поток не находится в состоянии ожидания, переходит к методу Join и ждет завершения потока.
  • Система снова переключается на поток с методом Spend , который обнаруживает, что счет пуст, и переводит поток в состояние ожидания вызовом метода WaitOne .
  • Поток StopThreads приостанавливается методом Jоin в ожидании завершения потока Spend , однако последний приостановлен в ожидании наличия денег.

В результате возникает взаимная блокировка, а приложение не завершится.

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

Однако на этот раз простым решением уже не обойтись.

На первый взгляд хочется заключить вызов Spend в функции KidsSpending в блок SyncLock и включить проверку m_Stopping перед вызовом метода Spend , как показано в следующем фрагменте:

If Not m_Stopping Then thiskid.Spend()

End SyncLock Loop Until m_Stopping

Вызов функции KillFamily тоже заключается в блок SyncLock .

Тем самым мы предотвращаем присваивание True переменной m_Stopping во время выполнения метода Spend . После успешного вызова KillFamily вызов thiskid Spend уже не состоится, поскольку проверка m_Stopping и вызов Spend находятся в одном блоке SyncLock .

К сожалению, такое решение приводит к взаимной блокировке, поскольку потоки, ожидающие объекта MoneyAvailable, удерживают блокировку и предотвращают выполнение не только KillFamily, но и любых других потоков, пытающихся вызвать Spend .

Честно говоря, я не вижу сколько-нибудь изящного решения для наших классов Account 1 . Один из возможных вариантов — установка тайм-аута для вызова Join . При обработке исключения следует найти приостановленный поток, прервать его и снова войти в Join . Это хлопотное и неуклюжее решение, но оно работает.

1 Принимаются предложения .

Наиболее правильный подход — заново спроектировать классы Account таким образом, чтобы в них были определены собственные методы Interrupt . При вызове из потока объект не только выходит из текущего ожидания, но и устанавливает флаг (конечно, должным образом синхронизируемый), чтобы в будущем он ни при каких условиях не вошел в состояние ожидания заново.

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

Многопоточность также сильно затрудняет тестирование. Все проблемы, описанные в этом разделе (кроме первой), носят в основном теоретический характер и на практике возникают крайне редко. Я знаю о них только потому, что внимательно проанализировал программу и спросил себя: «А что, если. » Если вы собираетесь писать многопоточные приложения, привыкните к мысли, что вам придется тщательно анализировать каждую строку программы, в которой потоки могут взаимодействовать посредством общих переменных или методов. Для поиска и решения многопоточных проблем следует использовать лучший отладчик из всех существующих — тот, что находится у вас в голове.

Помните о завершителях

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

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

Private Shared m_Random As New Random()

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

Но кто сказал, что этот объект безопасен по отношению к потокам?

Я не говорил. Более того, в документации об этом тоже ничего не сказано.

Запомните раз и навсегда: многие классы CLR не являются безопасными по отношению к потокам.

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

amount = int(m_Random.NextDouble() * 100)

в очень редких случаях вызывает ошибку переполнения.

Как может возникнуть переполнение, если m_Random. NextDouble всегда возвращает значение из интервала от 0 до 1? Никак, если только m_Random. NextDouble пo каким-то причинам не нарушает границы этого интервала. Это может объясняться либо ошибкой в программной реализации генератора случайных чисел (теоретически возможно), либо порчей данных, обусловленной тем, что объект небезопасен по отношению к потокам (гораздо более вероятно). За долгие часы тестирования эта ошибка возникла всего два раза; это в очередной раз доказывает, что с многопоточными проблемами следует бороться на уровне проектирования. Обнаружить их в процессе тестирования очень трудно.

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

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

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

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

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

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

В VB6 проблема эффективного ожидания решается плохо. Хотя ЕХЕ- приложения ActiveX могут создавать новые потоки, вызовы методов этих объектов обычно производятся синхронно. Следовательно, даже в случае приостановки потока ЕХЕ- приложения ActiveX , он не сможет вернуть управление вызывающему методу, что приведет к фактической блокировке работы клиента (а нередко и к тайм-ауту OLE Automation ). Компоненты, оформленные в виде ActiveX DLL, в Visual Basic 6 не могут надежно создавать потоки, поэтому операция ожидания приводит к приостановке потока клиентского приложения 1 .

В VB .NET вы просто порождаете новый объект потока, приказываете ему выполнить операцию ожидания (и таким образом войти в высокоэффективное состояние ожидания) и инициировать событие при ее завершении.

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

1 Замечу, что в распоряжении пользователей пакета Desaware SpyWorks уже давно находится компонент для работы с фоновыми потоками, упрощающий ожидание и общие фоновые операции в Visual Basic 6.

С концептуальной точки зрения идея фоновых операций проста. Конечно, все приложения этой главы в той или иной степени демонстрируют фоновые операции в многопоточном приложении. Программа Threading5 показывает, как использовать многопоточность для организации ожидания; фоновые операции всего лишь расширяют этот принцип. Если ваше приложение или компонент выполняет продолжительные операции (например, пересылку данных), ожидание конца этих операций обычно приводит к неприемлемому снижению быстродействия. Для примера возьмем текстовый редактор. Отдав команду сохранения файла, пользователь вполне может подождать ее завершения, но автоматическое сохранение резервной копии, периодически «замораживающее» приложение, вызвало бы сильное раздражение. Средства фонового выполнения операций — от фоновой печати до пересчета формул в электронной таблице, от построения сложных изображений до передачи данных по сети — играют важную роль во многих современных приложениях.

Трудность безопасной реализации многопоточности такого рода определяется сложностью приложения/компонента и фоновой операции.

Эффективный доступ со стороны клиента

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

Одна из главных причин, по которой программисты Visual Basic давно стремились к свободной многопоточности, заключается в том, что программы типа Internet Information Server (US) лучше всего работают с компонентами, использующими свободную потоковую модель. Сейчас я объясню, с чем это связано.

Каждый web-запрос, поступающий к IIS, полностью изолирован от всех остальных. Чтобы функции web-сайта выходили за рамки простого просмотра статических страниц, сервер должен располагать средствами для сохранения информации (состояния) между запросами от одного пользователя. Эту задачу можно решить разными способами, которые мы не рассматриваем, — достаточно просто сказать, что IIS позволяет web-приложениям сохранять информацию между запросами. Например, это позволяет создать web-приложение, которое читает информацию из базы данных, генерирует форму и отправляет ее пользователю. При поступлении следующего запроса от того же пользователя приложение продолжает работу и обрабатывает введенные данные. IIS обладает необходимыми средствами, чтобы написанное вами web-приложение могло сохранять информацию состояния между отправкой формы и получением заполненной формы от пользователя.

Для обеспечения максимальной эффективности IIS передает новые запросы любым свободным потокам.

Что произойдет, если ваше web-приложение использует объект Visual Basic 6 и потребует у IIS сохранить его?

Объект использует совместную потоковую модель. Это означает, что все обращения к методам и свойствам должны осуществляться из потока, создавшего объект. Если IIS сохраняет такой объект, он должен проследить за тем, чтобы все запросы текущего сеанса (для конкретного пользователя и конкретного приложения) передавались тому же самому потоку. А что, если поток занят и выполняет какую-то другую продолжительную операцию? В этом случае входящему запросу придется ждать его освобождения.

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

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

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

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

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

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

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

Оценка быстродействия в многопоточных приложениях

Один из моих любимых афоризмов звучит так:

«Существует ложь, наглая ложь и эталонные тесты» 1 .

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

1 Редактор потребовал, чтобы я дал ссылку па источник. Марк Твен однажды сказал: «Существует ложь, наглая ложь и статистика» — по я уверен, что он не упомянул эталонные тесты только потому, что в его время не существовало компьютеров.

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

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

В приложении ThreadPerformance определяется класс WorkerThread , выполняющий различные операции по запросу клиента. Этот класс также позволяет измерять продолжительность этих операций. В CLR определяется объект System.TimeSpan, представляющий промежуток времени и хорошо подходящий для хронометража. Свойство ElapsedTimeForCall возвращает ссылку на текущий объект TimeSpan .

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

Public read Private myTimeSpan As TimeSpan

Public Readonly Property ElapsedTimeForCall() As TimeSpan

Public LongDuration As Boolean

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

Dim counter As Long

Dim upperlimit As Long

Dim temp As Long

If LongDuration Then upperlimit = 5 * upperlimit For counter = 1

Next myTimeSpan = _

Метод SynchronousRequest отделяет присваивание переменной myTimespan от самой операции. Это необходимо, поскольку мы будем последовательно вызывать метод SynchronousOperation для каждого объекта, чтобы измерить быстродействие одного потока, последовательно обрабатывающего серию клиентских запросов. Нас интересует суммарное время, затраченное с начала первой операции, а не текущие затраты. В остальном метод SynchronousOperation идентичен методу WorkingOperation.

Public Sub SynchronousOperation()

Dim counter As Long

Dim upperlimit As Long

Dim temp As Long

If LongDuration Then upperlimit = 5 * upperlimit’

Методы SleepingOperation и SleepingSynchronous похожи на только что приведенные методы, за исключением того, что клиентские запросы не обеспечивают интенсивной загрузки процессора (листинг 7.24). Эти методы имитируют клиентские запросы, сопряженные с файловыми или сетевыми операциями, операциями с базами данных или интенсивным вводом-выводом без загрузки процессора.

Листинг 7.24. Методы SleepingOperation и SleepingSynchronous модуля WorkerThread

Dim sleepspan As Integer

If LongDuration Then sleepspan = sleepspan * 5

Public Sub SleepingSynchronous()

Dim sleepspan As Integer

If LongDuration Then sleepspan = sleepspan * 5

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

Метод RunTest2 (листинг 7.25) использует метод SleepingOperation для тестирования ситуаций без интенсивной загрузки процессора. В остальном этот метод практически идентичен RunTest .

Листинг 7.25. Модуль Modulel приложения ThreadingPerformance

Dim WorkerObjects(4) As WorkerThread

Dim Threads(4) As Threading.Thread

Sub RunTestO Dim x As Integer

‘ Создать потоки For x = 0 To 4

Threads(x) = New Threading.Thread(AddressOf _

‘ Запустить 5 потоков For x = 0 To 4

‘ Ожидать их завершения For x = 0 To 4

Sub RunTest2() Dim x As Integer

‘ Создать потоки For x = 0 To 4 Threads(x) = New

‘ Запустить 5 потоков For x = 0 To 4

‘ Ожидать их завершения For x = 0 To 4

Методы RunSynchronous и RunSynchronousZ сначала фиксируют время запуска для каждого объекта вызовом SynchronousRequest , а затем несколько раз вызывают SynchronousOperation или SleepingSynchronous , имитируя последовательную обработку клиентских запросов (листинг 7.26).

Листинг 7.26. Модуль Modulel приложения ThreadingPerformance (продолжение)

Dim x As Integer

Next For x = 0 To 4

Public Sub RunSynchronous2()

Dim x As Integer For x = 0 To 4

Метод ReportResults (листинг 7.27) выводит суммарные затраты времени по каждому объекту WorkerThread и среднее время по всем объектам. Главная программа проводит тестирование дважды: в первый раз все клиентские запросы имеют равную длину, а во второй раз первый клиентский запрос значительно длиннее остальных (для чего свойству LongDuration объекта WorkerThread задается значение True ).

Листинг 7.27. Модуль Modulel приложения ThreadingPerformance (продолжение)

Dim tot As Double

Dim ms As Double For x = 0 To 4

Console.Wri te(» Average: » + Int(tot / 5) .ToString())

Dim x As Integer

WorkerObjects(x) = New WorkerThread()

Console.WriteLine («Running tests. «)

Console.WriteLine («CPU-Intensive operations»)

Console.WriteLine («Synchronous Equal length operations»)

Console.WriteLine («Synchronous one long operation»)

Console.WriteLine («Multithreaded Equal length operations»)

Console.WriteLine («Multithreaded One long operations»)

Console.WriteLine («Non CPU-Intensive operations»)

Console.WriteLine («Synchronous Equal length operation»)

Console.WriteLine («Synchronous one long operation»)

Console.WriteLine («Multithreaded Equal length operations»)

Console.WriteLine («Multithreaded One long operations»)

Результаты работы программы ThreadPerformance

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

Synchronous Equal length operations

2093, 4186, 6238, 8271, 10284, Average: 6214

Synchronous one long operation

10184, 12217, 14250, 16303, 18346, Average: 14260

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

Среднее время равно 6,2 секунды, что достаточно близко к теоретическому среднему (6 секунд).

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

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

Multithreaded Equal length operations

8442. 8331, 8221, 8111, 7991, Average: 8219

Multithreaded One long operation

15241, 7921, 8301, 8181, 7921, Average: 9513

Когда все запросы имеют равную длину, время обработки каждого запроса заметно увеличивается. Дело в том, что многопоточность не следует воспринимать как магическое повышение мощности процессора — ресурсы процессора распределяются между потоками, что приводит к замедлению каждой операции. Суммарное время чуть превышает 8 секунд, что ниже теоретического значения (10 секунд). Вероятно, это объясняется тем, что алгоритм распределения процессорного времени операционной системой не сводится к простому делению 100 % доступного времени между потоками конкретного приложения. Когда процессорное время запрашивается несколькими потоками, ОС выделяет им больше процессорного времени, чем однопоточному приложению.

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

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

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

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

Илон Маск рекомендует:  Что такое код asp filterpath

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

Synchronous Equal length operation

1001, 2002, 3004. 5007, Average: 3004

Synchronous one long operation

5007, 6008, 7010, 8011, 9012, Average: 7010

Multithreaded Equal length operations

1001, 1001, 1001, 1001, 1001, Average: 1001

Multithreaded One long operation

5007, 1001, 1001, 1001, 1001, Average: 1802

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

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

Из этой главы вы узнали, что проблемы многопоточности обусловлены возможностью совместного доступа к данным со стороны нескольких потоков, а возможность прерывания операций на ассемблерном уровне заставляет предполагать, что прерывание может происходить внутри отдельных команд VB. Даже выполнение простейшей команды типа А=А+1 может завершиться неудачей, если несколько потоков, использующих общую переменную А, попытаются выполнить ее одновременно.

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

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

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

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

Приступая к рассмотрению принципов создания многопоточных приложений для среды 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-программисты обычно не обладают опытом проектирования и отладки многопоточных приложений» [1].

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

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

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

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

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

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

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

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

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

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

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

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

Далее запустите приложение и щелкните 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 добавить вывод текущего времени:

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

Рис. 1. Простейший вариант реализации многопоточного .NET-приложения.

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

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

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

Рис. 2. Параллельное выполнение потоков в консольном приложении.

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

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

Чтобы показать возможности управления запущенным процессом, добавим в конце процедуры 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 на такой код:

Запустите приложение — окно сообщения появится только после завершения контролируемого процесса. Можно вызывать метод Join с аргументом, чтобы задать время ожидания. При этом управление вернется после завершения потока (со значением функции True) или истечения тайм-аута (False). Вы можете сами убедиться в этом, написав такой код и меняя время тайм-аута для разных запусков приложения:

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

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

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

Рис. 3. Динамика выполнения двух потоков с равными приоритетами.

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

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

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

Рис. 4. Поток 2 был запущен позднее, но с более высоким приоритетом.

Заключение

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

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

«Если вы новичок, для вас может быть неожиданностью обнаружить, что издержки, связанные с созданием и диспетчеризацией потоков, могут привести к тому, что однопоточное приложение работает быстрее. Поэтому всегда старайтесь протестировать оба прототипа программы — однопоточный и многопоточный» [2].

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

«Не следует рассматривать применение многопоточности как подход по умолчанию» [2].

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

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

От себя добавлю, что технология создания многопоточных .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.

Другие статьи из раздела

  • Инструменты разработки Oracle для процессорных ускорителей
  • Платформа совместной разработки приложений от HP
  • Приложения для всех платформ – с единым кодом
  • Intel Parallel Composer – отладка параллельных программ
  • Intel Parallel Inspector – поиск ошибок многопоточности

Поместить в блог

Комментарии к статье

Рекламные ссылки

Chloride
Демонстрация Chloride Trinergy
Впервые в России компания Chloride Rus провела демонстрацию системы бесперебойного электропитания Chloride Trinergy®, а также ИБП Chloride 80-NET™, NXC и NX для своих партнеров и заказчиков.

NEC Нева Коммуникационные Системы
Завершена реорганизация двух дочерних предприятий NEC Corporation в России
С 1 декабря 2010 года Генеральным директором ЗАО «NEC Нева Коммуникационные Системы» назначен Раймонд Армес, занимавший ранее пост Президента Shyam …

компания «Гротек»
С 17 по 19 ноября 2010 в Москве, в КВЦ «Сокольники», состоялась VII Международная выставка InfoSecurity Russia. StorageExpo. Documation’2010.
Новейшие решения защиты информации, хранения данных и документооборота и защиты персональных данных представили 104 организации. 4 019 руководителей …

МФУ Panasonic DP-MB545RU с возможностью печати в формате А3
Хотите повысить эффективность работы в офисе? Вам поможет новое МФУ #Panasonic DP-MB545RU. Устройство осуществляет

Adaptec by PMC
RAID-контроллеры Adaptec Series 5Z с безбатарейной защитой кэша
Опытные сетевые администраторы знают, что задействование в работе кэш-памяти RAID-контроллера дает серьезные преимущества в производительности …

Chloride
Трехфазный ИБП Chloride от 200 до 1200 кВт: Trinergy
Trinergy — новое решение на рынке ИБП, впервые с динамическим режимом работы, масштабируемостью до 9.6 МВт и КПД до 99%. Уникальное сочетание …

Реферат: Потоки в Visual Basic

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

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

Эта идея неверна. Развертывание технологии должно управляться прежде всего проблемой, которую необходимо решить решить, а не технологией, которую кто-то пробует Вам впарить;).

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

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

Недавние статьи в Microsoft Systems Journal и Visual Basic Programmer’s Journal представили программистам на Visual Basic возможность использования функции API CreateThread, чтобы непосредственно поддерживать многопоточный режим под Visual Basic. После этого, один читатель пожаловался, что моя книга Visual Basic Programmer’s Guide to the Win32 API является неполной, потому что я не описал в ней эту функцию и не продемонстрировал эту технологию. Эта статья — частично является ответом этому читателю, и частично — ответом на другие статьи, написанными на эту тему. Эта статья также является дополнением к главе 14 моей книги «Разработка ActiveX компонент на Visual Basic 5.0» относительно новых возможностей, обеспечиваемых Visual Basic 5.0 Service Pack 2.


Быстрый обзор Многопоточности

Если Вы уже хорошо разбираетесь в технологии многопоточного режима, то Вы можете пропустить этот раздел и продолжать чтение с раздела, названного «Что нового в Service Pack 2.»

Каждый, кто использует Windows, знает, что Windows способно делать больше чем одну вещь одновременно.

Может одновременно выполнять несколько программ, при одновременном проигрывании компакт-диска, посылке факса и пересылке файлов. Каждый программист знает (или должен знать) что ЦЕНТРАЛЬНЫЙ ПРОЦЕССОР компьютера может только выполнять одну команду одновременно (проигнорируем существование многопроцессорных машин). Как единственный ЦЕНТРАЛЬНЫЙ ПРОЦЕССОР может выполнять множество задач?

Это делается быстрым переключением между многими задачами. Операционная система содержит в памяти все программы, которые запущены в настоящий момент. Это позволяет ЦЕНТРАЛЬНОМУ ПРОЦЕССОРУ выполнять программы по очереди. Каждый раз происходит переключение между программами, при этом меняется содержимое внутренних регистров, включая указатель команды и указатель вершины стека. Каждая из таких «задач» называется потоком выполнения (thread of execution).

В простой многозадачной системе, каждая программа имеет емеет единственный поток. Это означает, что ЦЕНТРАЛЬНЫЙ ПРОЦЕССОР начинает выполнение команд в начале программы и продолжает следуя инструкциям в последовательности, определенной программой до тех пор, пока программа не завершается.

Скажем, программа имеет пять команд: B C D и E, которые выполняются последовательно (никаких переходов нет в этом примере). Когда приложение имеет один поток, команды будут всегда выполнять в точно том же самом порядке: A, B, C, D и E. Действительно, ЦЕНТРАЛЬНЫЙ ПРОЦЕССОР может потребовать времени для выполнения других команд в других программах, но они не будут влиять на это приложение, если не имеется конфликт над общими ресурсами системы, но это уже отдельная тема для разговора.

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

В каком порядке будут следовать команды в этом приложении?

Это могло бы быть:

Thread 1 A B C D E E

Thread 1 A B C D E

Thread 1 A B C D E

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

Почему — это проблема?

Рассмотрим проект MTDemo:

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

‘ MTDemo — Multithreading Demo program

‘ Copyright © 1997 by Desaware Inc. All Rights Reserved

Public GenericGlobalCounter As Long

Public TotalIncrements As Long

‘ Этот проект содержит одну форму — frmMTDemo1, которая содержит

‘ MTDemo — Multithreading Demo program

‘ Copyright © 1997 by Desaware Inc. All Rights Reserved

Dim State As Integer

‘ State = 1 — Loading existing value

‘ State = 2 — Adding 1 to existing value

‘ State = 3 — Storing existing value

‘ State = 4 — Extra delay

Dim Accumulator As Long

Const OtherCodeDelay = 10

Private Sub Command1_Click()

Dim f As New frmMTDemo1

Private Sub Form_Load()

Timer1.Interval = 750 + Rnd * 500

Private Sub Timer1_Timer()

Select Case State

lblOperation = «Loading Acc»

Accumulator = Accumulator + 1

TotalIncrements = TotalIncrements + 1

lblOperation = «Generic Code»

If otherdelay >= OtherCodeDelay Then

otherdelay = otherdelay + 1

Public Sub UpdateDisplay()

Эта программа для моделирования многопоточного режима использует таймер и простой конечный автомат. Переменная State описывает пять команд, которые эта программа выполняет. State = 0 — неактивное состояние. State = 1 загружает локальную переменную глобальной переменной GenericGlobalCounter. State = 2 увеличивает на единицу локальную переменную. State = 3 запоминает результат в переменной GenericGlobalCounter и увеличивает переменную TotalIncrements (которая считает количество приращений переменной GenericGlobalCounter). State = 3 добавляет дополнительную задержку, представляющую собой время, затраченное на выполнение других команд в программе.

Функция UpdateDisplay обновляет три метки на форме, которые показывают текущее значение переменной GenericGlobalCounter, локального сумматора, и общего количества приращений.

Каждый сигнал таймера моделирует цикл ЦЕНТРАЛЬНОГО ПРОЦЕССОРА в текущем потоке. Если Вы запустите программу, то увидете, что значение переменной GenericGlobalCounter будет всегда точно равно переменной TotalIncrements, потому что переменная TotalIncrements показывает количество увеличений счетчика GenericGlobalCounter потоком.

Но что случится, когда Вы нажимаете кнопку Command1 и запустите второй экземпляр формы? Эта новая форма смоделирует второй поток.

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

Другими словами, переменная была увеличена дважды, но значение увеличилось только на единицу. Если Вы запускаете несколько форм, то сразу заметите, что число приращений, представляемой переменной TotalIncrements, растет намного быстрее, чем счетчик GenericGlobalCounter.

Что, если переменная представляет объектный счет блокировки — который следит, когда объект должен быть освобожден? Что, если она представляет собой сигнал, который указывает, что ресурс находится в использовании?

Такая проблема может привести к появлению ресурсов, постоянно недоступных в системе, к объекту, блокируемому в памяти, или преждевременно освобожденному. Это может привести к сбоям приложения.

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

Решение проблем Многопоточности

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

Избегайте всеобщего использования глобальных переменных.

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

Первый подход используется в основном в Visual Basic. Когда Вы включаете многопоточный режим в Visual

Basic приложения, все глобальные переменные станут локальными для специфического потока. Это свойственно способу, с которым Visual Basic выполняет apartment model threading — подробнее об этом позднее.

Первоначальный выпуск Visual Basic 5.0 позволял использовать многопоточность только в компонентах, которые не имели никаких элементов пользовательского интерфейса. Так было потому что они не имели безопасного потока управления формами. Например: когда Вы создаете форму в Visual Basic, VB дает ей имя глобальной переменной (таким образом, если Вы имеете форму, именованную Form1, Вы можете непосредственно обращаться к ее методам, используя Form1.метод вместо того, чтобы объявить отдельную переменную формы). Этот тип глобальной переменной может вызывать проблемы многопоточного режима, которые Вы видели ранее. Имелись несомненно другие проблемы внутри управления формами.

С service pack 2, управление формами Visual Basic было сделано безопасным потоком. Это говорит о том, что каждый поток имеет собственную глобальную переменную для каждой формы, определенной в проекте.

Что нового в Service Pack 2

Сделав поток управления формами безопасным, Service pack 2 предоставил возможность с помощью Visual

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

Приложение должно быть определено как программа ActiveX Exe с установкой запуска из Sub Main:

‘ MTDemo2 — Multithreading demo program

‘ Copyright © 1997 by Desaware Inc. All Rights Reserved

Declare Function FindWindow Lib «user32» Alias «FindWindowA» _

(ByVal lpClassName As String, ByVal lpWindowName As String) _

Dim f As frmMTDemo2

‘ We need this because Main is called on each new thread

Dim hwnd As Long

hwnd = FindWindow(vbNullString, «Multithreading Demo2»)

If hwnd = 0 Then

Set f = New frmMTDemo2

Первый раз программа загружает и отображает основную форму приложения. Подпрограмма Main должна выяснить, является ли это первым потоком приложения, поэтому этот код выполняется при старте каждого потока. Вы не можете использовать глобальную переменную, чтобы это выяснить, потому что Visual Basic apartment model хранит глобальные переменные специфическими для одиночного потока. В этом примере используется функция API FindWindow, чтобы проверить, была ли загружена основная форма примера.

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

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

‘ MTDemo2 — Multithreading demo program

‘ Copyright © 1997 by Desaware Inc. All Rights Reserved

Private Sub Class_Initialize()

Dim f As New frmMTDemo2

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

‘ MTDemo2 — Multithreading demo program

‘ Copyright © 1997 by Desaware Inc. All Rights Reserved

Private Sub cmdLaunch1_Click()

Dim c As New clsMTDemo2

Private Sub cmdLaunch2_Click()

Dim c As clsMTDemo2

Set c = CreateObject(«MTDemo2.clsMTDemo2»)

Private Sub Form_Load()

Форма отображает идентификатор потока в метке на форме. Форма содержит две командные кнопки, одна из которых использует оператор New, другая -использует оператор CreateObject.

Если Вы запустите программу внутри среды Visual Basic, то увидите, что формы всегда создаются в одном и том же потоке. Это происходит, потому что среда Visual Basic поддерживает только одиночный поток. Если Вы скомпилируете и запустите программу, то увидите, что подход, использующий CreateObject создает и clsMTDemo2 и ее форму в новом потоке.

Откуда вся суета относительно многопоточного режима, если он включает так много потенциальной опасности? Потому что, в некоторых ситуациях, многопоточный режим может значительно улучшать эффективность приложения. В некоторых случаях это может улучшать эффективность некоторых операций синхронизации типа ожидания завершения приложения. Это позволяет сделать архитектуру приложения более гибкой. Например, операция Add a long в форме MTDEMO2 со следующим кодом:

Private Sub cmdLongOp_Click()

For l = 1 To 1000000

s = Chr$(l And &H7F)

Запустите несколько экземпляров формы, используя кнопку cmdLaunch1. Когда Вы нажимаете на кнопку cmdLongOp на любой из форм, то увидите, что это действие замораживает операции на всех других формах. Так происходит, потому что все формы выполняются в одиночном потоке — и этот поток занят выполнением длинного цикла. Если Вы запустите несколько экземпляров формы кнопкой cmdLaunch2 и нажимете кнопку cmdLongOp на форму, то только эта форма будет заморожена — другие формы будут активными. Они выполняются в собственных потоках, и длинный цикл будет выполняться только в собственном потоке.

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

Дальше краткое резюме, когда важен многопоточный режим:

Сервер ActiveX EXE – без общих ресурсов.

Когда Вы имеете ActiveX EXE сервер, который Вы собираетесь совместно использовать среди нескольких приложений, многопоточный режим предотвращает приложения от нежелательных взаимодействий с друг другом. Если одно приложение выполняет длинную операцию на объекте в однопоточном сервере, другие приложения будут вытеснены, т.е. будут ждать, когда освободится сервер. Многопоточный режим рещает эту проблему. Однако, имеются случаи, где Вы можете хотеть использовать ActiveX EXE сервер, чтобы регулировать доступ к общедоступнному ресурсу (shared resource). Например, сервер stock quote, описанный в моей книге Developing ActiveX Components. В этом случае сервер stock quote выполняется в одиночном потоке и который доступен для всех приложений, использующих сервер по очереди.

Многопоточный клиент – выполняемый как ActiveX EXE сервер

Простая форма этого подхода продемонстрирована в приложении MTDEMO2. Этот подход используется, когда приложение поддерживает множественные окна, которые должны исходить из одного приложения, но работать полностью независимо. Интернет-браузер — хороший пример такого многопоточного клиента, где каждое окно выполняется в собственном потоке. Здесь следует обратить внимание на то, что многопоточный режим не должен использоваться как замена для хорошего событийно управляемого проекта.

Многопоточные серверы DLL или EXE

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

Соглашение о потоках

Верите или нет, но все это было введением. Часть этого материала является обзором материала, который описан в моей книге Developing ActiveX Components, другая часть материала описывает новую информацию для service pack 2.

Теперь, позволите задавать вопрос, который имеет отношение к многопоточному режиму, использующему COM (модель многокомпонентных объектов, на которой основаны не только все Visual Basic объекты, но и другие windows приложения, использующие технологии OLE).

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

Как это возможно, что Visual Basic позволяет Вам создавать объекты и использовать их с одиночными и многопоточными средами безотносительно к тому, разработаны ли они для одиночного или многопоточного использования?

Другими словами — Как многопоточные Visual Basic приложения могут использовать объекты, которые не разработаны для безопасного выполнения в многопоточной среде? Как могут другие многопоточные приложения использовать однопоточные объекты Visual Basic?

Коротко: как COM поддерживает потоки?

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

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

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

Модель одиночного потока:

Однопоточный сервер — самый простой тип реализации сервера. И самый простой для понимания.

В этом случае EXE сервер выполняется в одиночном потоке. Все объекты создаются в этом потоке.

Все вызовы методов каждого объекта, поддерживаемого сервером должны прибыть в этот поток.

Но что будет, если клиент выполняется в другом потоке? В том случае, для объекта сервера должен быть создан промежуточный объект (proxy object). Этот промежуточный объект выполняется в потоке клиента и отражает методы и свойства фактического объекта. Когда вызывается метод промежуточного объекта, он выполняет операции, необходимые для подключению к потоку объекта, а затем вызывает метод фактического объекта, используя параметры, переданные к промежуточному объекту. Естественно, что этот подход требует значительного времени на выполнение задачи, однако он позволяет выполнить все соглашения. Этот процесс переключения потоков и пересылки данных от промежуточного объекта к фактическому объекту и обратно называется marshalling. Эта тема обсуждается в главе 6 моей книги Developing ActiveX Components.

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

Модель Apartment Threading

Обратите внимание, что модель Apartment Threading как определено COM не требует, чтобы каждый поток имел собственный набор глобальных переменных. Visual Basic таким образом реализует модель Apartment Threading. Модель Apartment Threading декларирует, что каждый объект может быть создан в собственном потоке, однако, как только объект создан, его методы и свойства могут вызываться только тем же самым потоком, которая создал объект. Если объект другого потока захочет иметь доступ к методам этого объекта, то он должен действовать через промежуточный объект.

Такая модель относительно проста для реализации. Если Вы устраняете глобальные переменные (как делает Visual Basic), модель Apartment Threading автоматически гарантирует безопасность потока — так как каждый объект действительно выполняется в собственном потоке, и благодаря отсутствию глобальных переменных, объекты в разных потоках не взаимодействуют друг с другом.

Модель свободных потоков

Модель свободных потоков (Free Threading Model) заключается в следующем.. Любой объект может быть создан в любом потоке. Все методы и свойства любого объекта могут быть вызываны в любое время из любого потока. Объект принимает на себя всю ответственность за обработку любой необходимой синхронизации.

Это самая трудная в реализации модель, так как требуется, чтобы всю синхронизацию обрабатывал программист. Фактически до недавнего времени, технология OLE непосредственно не поддерживала эту модель! Однако, с тех пор marshalling никогда не требуется и это наиболее эффективная модель потоков.

Какую модель поддерживает ваш сервер?

Как приложение или сама Windows узнает, которую модель потоков использует сервер? Эта информация включена в системный реестр (registry). Когда Visual Basic создает объект, он проверяет системный реестр, чтобы определить, в каких случаях требуется использовать промежуточный объект (proxy object) и в каких — marshalling.

Эта проверка является обязанностью клиента и необходима для строгой поддержки требований многопоточности для каждого объекта, которого он создает.

Функция API CreateThread

Теперь давайте посмотрим, как с Visual Basic может использоваться функция API CreateThread. Скажем, Вы имеете класс, что Вы хотите выполненять в другом потоке, например, чтобы выполнить некоторую фоновую операцию. Характерный класс такого типа мог бы иметь следующий код (из примера MTDemo 3):

‘ MTDemo 3 — Multithreading example

‘ Copyright © 1997 by Desaware Inc. All Rights Reserved

Public Function DoTheCount(ByVal finalval&) As Boolean

Dim s As String

s$ = «In Thread » & App.threadid

Call MessageBox(0, s$, «», 0)

If l >= finalval Then

Call MessageBox(0, «Done with counting», «», 0)

Класс разработан так, чтобы функция DoTheCount могла неоднократно вызываться из непрерывного цикла в фоновом потоке. Мы могли бы поместить цикл непосредственно в сам объект, но вы вскоре увидите, что были веские причины для проектирования объекта как показано в примере. При первом вызове функции DoTheCount появляется MessageBox, в котором показан идентификатор потока, по которому мы можем определить поток, в котором выполняется код. Вместо VB команды MessageBox используется MessageBox API, потому что функция API, как известно, поддерживает безопасное выполнение потоков. Второй MessageBox появляется после того, как закончен подсчет и сгенерировано событие, которое указывает, что операция закончена.

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

Private Sub cmdCreateFree_Click()

Set c = New clsBackground

Функция StartBackgroundThreadFree определена в модуле modMTBack следующим образом:

Declare Function CreateThread Lib «kernel32» _

(ByVal lpSecurityAttributes As Long, ByVal _

dwStackSize As Long, ByVal lpStartAddress As Long, _

ByVal lpParameter As Long, ByVal dwCreationFlags _

As Long, lpThreadId As Long) As Long

Declare Function CloseHandle Lib «kernel32» _

(ByVal hObject As Long) As Long

‘ Start the background thread for this object

‘ using the invalid free threading approach.

Public Function StartBackgroundThreadFree _

(ByVal qobj As clsBackground)

Dim threadid As Long

Dim threadparam As Long

‘ Free threaded approach

hnd = CreateThread(0, 2000, AddressOf _

BackgroundFuncFree, threadparam, 0, threadid)

‘ Return with zero (error)

‘ We don’t need the thread handle

Функция CreateThread имеет шесть параметров:

lpSecurityAttributes — обычно устанавливается в нуль, чтобы использовать заданные по умолчанию атрибуты защиты.

dwStackSize — размер стека. Каждый поток имеет собственный стек.

lpStartAddress — адрес памяти, где стартует поток. Он должен быть равен адресу функции в стандартном модуле, полученном при использовании оператора AddressOf.

lpParameter — long 32 разрядный параметр, который передается функции, запускающей новый поток.

dwCreationFlags — 32 бит переменная флагов, которая позволяет Вам управлять запуском потока (активный, приостановленный и т.д.). Подробнее об этих флагах можно почитать в Microsoft’s online 32 bit reference.

lpThreadId — переменная, в которую загружается уникальный идентификатором нового потока.

Функция возвращает дескриптор потока.

В этом случае мы передаем указатель на объект clsBackground, который мы будем использовать в новом потоке. ObjPtr восстанавливает значение указателя интерфейса в переменную qobj. После создания потока закрывается дескриптор при помощи функции CloseHandle. Это действие не завершает поток, — поток продолжает выполняться до выхода из функции BackgroundFuncFree. Однако, если мы не закрыли дескриптор, то объект потока будет существовать даже после выхода из функции BackgroundFuncFree. Все дескрипторы

потока должны быть закрыты и при завершении потока система освобождает занятые потоком ресурсы.

Функция BackgroundFuncFree имеет следующий код:

‘ A free threaded callback.

‘ A free threaded callback.

‘ This is an invalid approach, though it works

Public Function BackgroundFuncFree(ByVal param As _

IUnknown) As Long

Dim qobj As clsBackground

‘ Free threaded approach

Set qobj = param

Do While Not qobj.DoTheCount(100000)

‘ Thread ends on return

Параметром этой функции является- указатель на интерфейс (ByVal param As IUnknown). При этом мы можем избежать неприятностей, потому что под COM каждый интерфейс основывается на IUnknown, так что такой тип параметра допустим независимо от типа интерфейса, передаваемого функции. Мы, однако, должны немедленно определить param как тип объекта, чтобы затем его использовать. В этом случае qobj установливается как объект clsBackground, который был передан к объекту StartBackgroundThreadFree.

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

Доступ к объекту qobj чрезвычайно быстр из-за использования подхода свободного потока (free threading) — никакая переадресация (marshalling) при этом не используется.

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

Некоторые разработчики, кто пробовали развертывать приложения, применяющие этот тип многопоточности, обнаружили, что их приложения вызывают сбои после обновления к VB5 service pack 2.

Является ли это дефектом Visual Basic?

Означает ли это, что Microsoft не обеспечила совместимость?

Ответ на оба вопроса: Нет

Проблема не в Microsoft или Visual Basic.

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

Проблема проста — Visual Basic поддерживает объекты и в модели одиночного потока и в apartment model.

Позвольте мне перефразировать это: объекты Visual Basic являются COM объектами и они,согласно COM соглашению, будут правильно работать как в модели одиночного потока так и в apartment model. Это означает, что каждый объект ожидает, что любые вызовы методов будут происходить в том же самом потоке, который создал объект.

Пример, показанный выше, нарушает это правило.

Это нарушает соглашение COM.

Что это означает?

Это означает, что поведение объекта подчиненно изменениям, так как Visual Basic постоянно модифицируется.

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

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

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

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

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

Этот подход является программной алхимией. Это безответственно и ни один программист не должен когда-либо использовать это. Точка.

Обратно к функции API CreateThread

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

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

Пример MTDEMO3 демонстрирует этот подход в форме frmMTDemo3, имеющей код, который запускает класс фона в apartment model следующим образом:

Private Sub cmdCreateApt_Click()

Set c = New clsBackground

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

‘ Structure to hold IDispatch GUID

Data2 As Integer

Data3 As Integer

Public IID_IDispatch As GUID

Declare Function CoMarshalInterThreadInterfaceInStream Lib _

«ole32.dll» (riid As GUID, ByVal pUnk As IUnknown, _

ppStm As Long) As Long

Declare Function CoGetInterfaceAndReleaseStream Lib _

«ole32.dll» (ByVal pStm As Long, riid As GUID, _

pUnk As IUnknown) As Long

Declare Function CoInitialize Lib «ole32.dll» (ByVal _

pvReserved As Long) As Long

Declare Sub CoUninitialize Lib «ole32.dll» ()

‘ Start the background thread for this object

‘ using the apartment model

‘ Returns zero on error

Public Function StartBackgroundThreadApt(ByVal qobj _

Dim threadid As Long

Dim threadparam As Long

Dim tobj As Object

‘ Proper marshaled approach

res = CoMarshalInterThreadInterfaceInStream _

(IID_IDispatch, qobj, threadparam)

hnd = CreateThread(0, 2000, AddressOf _

BackgroundFuncApt, threadparam, 0, threadid)

‘ Return with zero (error)

‘ We don’t need the thread handle

Функция StartBackgroundThreadApt немного более сложна чем ее эквивалент при применении подхода свободных потоков. Первая новая функция называется InitializeIID. Она имеет следующий код:

‘ Initialize the GUID structure

Private Sub InitializeIID()

Static Initialized As Boolean

If Initialized Then Exit Sub

Вы видите, нам необходим идентификатор интерфейса — 16 байтовая структура, которая уникально определяет интерфейс. В частности нам необходим идентификатор интерфейса для интерфейса IDispatch (подробная информация относительно IDispatch может быть найдена в моей книге Developing ActiveX Components). Функция InitializeIID просто инициализирует структуру IID_IDISPATCH к корректным значениям для идентификатора интерфейса IDispatch. Значение Это значение получается с помощью использования утилиты просмотра системного реестра.

Почему нам необходим этот идентификатор?

Потому что, чтобы твердо придерживаться соглашения COM о потоках, мы должны создать промежуточный объект (proxy object) для объекта clsBackground. Промежуточный объект должен быть передан новому потоку вместо первоначального объекта. Обращения к новому потоку на промежуточном объекте будут переадресованы (marshaled) в текущий поток.

CoMarshalInterThreadInterfaceInStream выполняет интересную задачу. Она собирает всю информацию, необходимую при создании промежуточного объекта, для определенного интерфейса и загружает ее в объект потока (stream object). В этом примере мы используем интерфейс IDispatch, потому что мы знаем, что каждый класс Visual Basic поддерживает IDispatch и мы знаем, что поддержка переадресации (marshalling) IDispatch встроена в Windows — так что этот код будет работать всегда. Затем мы передаем объект потока (stream object) новому потоку. Этот объект разработан Windows, чтобы быть передаваемым между потоками одинаковым способом, так что мы можем безопасно передавать его функции CreateThread. Остальная часть функции StartBackgroundThreadApt идентична функции StartBackgroundThreadFree.

Функция BackgroundFuncApt также сложнее чем ее эквивалент при использовании модели свободных потоков и показана ниже:

‘ A correctly marshaled apartment model callback.

‘ This is the correct approach, though slower.

Public Function BackgroundFuncApt(ByVal param As Long) As Long

Dim qobj As Object

Dim qobj2 As clsBackground

‘ This new thread is a new apartment, we must

‘ initialize OLE for this apartment

‘ (VB doesn’t seem to do it)

‘ Proper apartment modeled approach

res = CoGetInterfaceAndReleaseStream(param, _

Set qobj2 = qobj

Do While Not qobj2.DoTheCount(10000)

‘ Alternatively, you can put a wait function here,

‘ then call the qobj function when the wait is satisfied

‘ All calls to CoInitialize must be balanced

Первый шаг должен инициализировать подсистему OLE для нового потока. Это необходимо для переадресации (marshalling) кода, чтобы работать корректно. CoGetInterfaceAndReleaseStream создает промежуточный объект для объекта clsBackground и реализует объект потока (stream object), используемый для передачи данных из другого потока. Интерфейс IDispatch для нового объекта загружается в переменную qobj.

Теперь возможно получить другие интерфейсы — промежуточный объект будет корректно переадресовывать данные для каждого интерфейса, который может поддерживать.

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

Хорошим результатом применения этого подхода является то, что все работает правильно. Объект clsBackground может безопасно показывать формы и генерировать события. Недостатком этого подхода является, конечно, его более медленное исполнение. Переключение потоков и переадресация (marshalling) — относительно медленные операции. Вы фактически никогда не захотите выполнять фоновую операцию как показано здесь.

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

Что, если Вы хотите выполнить фоновую операцию, которая не должна использовать объект? Очевидно, проблемы с соглашением COM о потоках исчезают. Но появляются другие проблемы. Как фоновый поток сообщит о своем завершении приоритетному потоку? Как они обмениваются данными? Как два потока будут синхронизированы? Все это возможно выполнить с помощью соответствующих вызовов API. В моей книге Visual Basic 5.0 Programmer’s Guide to the Win32 API имеется информации относительно объектов синхронизации типа Событий, Mutexes, Семафоров и Waitable Таймеров.

Эта книга также включает примеры файлов отображаемых в память, которые могут быть полезны при обмене данных между процессами. Вы сможете использовать глобальные переменные, чтобы обмениваться данные, но надо знать, что такое поведение не гарантируется Visual Basic(другими словами, даже если это сейчас работает, не имеется никаких гарантий, что это будет работать в будущем). В этом случае я мог бы предложить Вам использовать для обмена данными методики, основанные на API. Однако, преимуществом показанного здесь подхода, основанного на объектах, является то, что этот подход делает проблему обмена данными между потоками тривиальной, просто делайте это через объект.

Я однажды услышал от опытного программиста под Windows, что OLE является самой трудной технологией, которой он когда-либо обучался. Я с этим согласен. Это очень обширная тема, и некоторые части этой технологии очень трудно понять. Visual Basic, как всегда, скрывает от Вас много сложностей.

Имеется сильное искушение, чтобы пользоваться преимуществом продвинутых методов типа многопоточного режима, используя подход «tips and techniques». Это искушение поощрено некоторыми статьями, которые иногда представляют специфическое решение, приглашая Вас вырезать и вставить (cut and past) их методики в ваши собственные приложения.

Когда я писал книгу Visual Basic Programmer’s Guide to the Windows API, я выступал против такого подхода к программированию. Я чувствовал, что вообще безответственно включать в приложение код, который Вы не понимаете, и что реальное знание, которое так тяжело получить, стоит затраченных усилий.

Таким образом мои книги по API были разработаны, чтобы обеспечить не быстрые ответы и простые решения, а чтобы обучить использованию API к такой степени, что программисты могли бы интеллектуально правильно применять даже наиболее продвинутые методы. Я применил это тот же самый подход к моей книге Developing ActiveX Components, которая требует много времени для обсуждения принципов ActiveX, COM и объектно-ориентированного программирования перед описанием подробностей реализации этой технологии.

Многое из моей карьеры на ниве Visual Basic и многое из деятельности в фирме Desaware, основано на обучении Visual Basic программистов продвинутым методам. Читатель, кто вдохновил меня на написание этой статьи, критикуя меня за сдерживание технологии многопоточности, пропустил точку.

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

Они должны быть поддерживаемыми в конечном счете. Они не должны разрушаться, когда изменяются Windows или Visual Basic.

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

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

Я не могу обещать, что использование apartment model версии CreateThread является абсолютно корректным, но мое понимание проблемы и опыт показывают, что это безопасно.

Могут иметься другие факторы, которые я пропустил. OLE — действительно сложная вещь и модули OLE DLL и сам Visual Basic подвержены изменениям. Я только могу утверждать, что лучшее из моего знания — код, который я здесь показал, удовлетворяет правилам COM и что эмпирическое доказательство показывает, что Visual Basic runtime 5 0’s является достаточно безопасным для выполнения фонового кода потока в стандартном модуле.

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

Кто-нибудь знает, как заставить VBA запускать несколько streamов? Я использую Excel.

7 Solutions collect form web for “Многопоточность в VBA”

Не может быть сделано с VBA. VBA построен в однопоточной квартире. Единственный способ получить несколько streamов – построить DLL в чем-то другом, кроме VBA, который имеет COM-интерфейс и вызывает его из VBA.

INFO: описания и разработки моделей streamов OLE

Как вы, наверное, узнали, что VBA не поддерживает multithreading, но. Существует 3 способа достижения многопоточности:

  1. COM / dll – например, C # и class Parallel для запуска в отдельных streamах
  2. Использование рабочих streamов VBscript – запуск кода VBA в отдельных streamах VBscript
  3. Использование рабочих streamов VBA, выполняемых, например, посредством VBscript – копирование книги Excel и параллельная работа с макросом.

Учитывая подход №3, я также создал инструмент многопоточности VBA, который позволяет легко добавлять multithreading в VBA: http://analystcave.com/excel-vba-multithreading-tool/

См. Примеры ниже:

Многопоточность для цикла

Запуск макроса Excel асинхронно

Я искал что-то подобное, и официальный ответ – нет. Тем не менее, я смог найти интересную концепцию Дэниела на ExcelHero.com.

В принципе, вам нужно создать рабочие vbscripts для выполнения различных вещей, которые вы хотите, и вернуть отчет в Excel. Для того, что я делаю, получая данные HTML с разных сайтов, он отлично работает!

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

Если мотивация многопоточности должна иметь более гибкий пользовательский интерфейс, который не зависает, когда выполняется длительный код, VBA имеет несколько низкотехнологичных решений, которые часто работают на практике:

1) Пользовательские формы могут быть сделаны для демонстрации безмолвно – что позволяет пользователю взаимодействовать с Excel, когда форма открыта. Это можно указать во время выполнения, установив свойство ShowModal Userform в false или может быть выполнено динамически, поскольку от нагрузок, помещая строку

в событии инициализации формы пользователя.

2) Инструкция DoEvents. Это приводит к тому, что VBA уступает управлению ОС для выполнения любых событий в очереди событий, включая события, генерируемые Excel. Типичным вариантом использования является обновление диаграммы во время выполнения кода. Без DoEvents диаграмма не будет перерисовываться до тех пор, пока макрос не будет запущен, но с помощью Doevents вы можете создавать анимированные диаграммы. Вариант этой идеи – общий трюк создания индикатора прогресса. В цикле, который должен выполнить 10 000 000 раз (и управляется индексом цикла i ), вы можете иметь раздел кода, например:

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

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

Это все VBA (в отличие от других ответов)

Функция, выполняющая асинхронно под-функцию

Модуль формы, необходимый для достижения этого

(имя формы = MultiThreadingEngine, не имеет каких-либо элементов управления или свойств)

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

Как уже говорилось, VBA не поддерживает multithreading.

Но вам не нужно использовать C # или vbScript для запуска других рабочих streamов VBA.

Я использую VBA для создания рабочих streamов VBA .

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

Затем вы можете запускать новые экземпляры Excel (работа в другом streamе), просто создав экземпляр Excel.Application (чтобы избежать ошибок, я должен установить новое приложение на видимое).

Чтобы фактически запустить некоторую задачу в другом streamе, я могу запустить макро в другом приложении с параметрами из основной книги.

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

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

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

November 2020

75.7k раз

Есть ли здесь кто-нибудь знает, как получить VBA для запуска нескольких потоков? Я использую Excel.

7 ответы

As said before, VBA does not support Multithreading.

But you don’t need to use C# or vbScript to start other VBA worker threads.

I use VBA to create VBA worker threads.

First copy the makro workbook for every thread you want to start.

Then you can start new Excel Instances (running in another Thread) simply by creating an instance of Excel.Application (to avoid errors i have to set the new application to visible).

To actually run some task in another thread i can then start a makro in the other application with parameters form the master workbook.

To return to the master workbook thread without waiting i simply use Application.OnTime in the worker thread (where i need it).

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

Не может быть сделано изначально с помощью VBA. VBA встроен в однопоточных квартиру. Единственный способ получить несколько потоков, чтобы построить DLL в нечто иное, чем VBA, который имеет COM-интерфейса и вызывать из VBA.

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

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

1) UserForms может быть сделано для отображения modelessly — который позволяет пользователю взаимодействовать с Excel, а форма открыта. Это может быть определено во время выполнения, установив ShowModal свойство UserForm к ложным или может быть сделано динамически, как от нагрузок, поставив линию

в случае инициализации формы записи пользователя.

2) DoEvents заявление. Это приводит к VBA , чтобы передать контроль в ОС для выполнения каких — либо событий в очереди событий — в том числе событий , генерируемых Excel. Типичный вариант использования обновляет график во время выполнения кода. Без DoEvents график не будет перекрасили , пока макрос запускается, но с DoEvents вы можете создавать анимированные графики. Разновидность этой идеи является обычным трюком создания индикатора хода. В цикле , который должен выполнить 10000000 раз (и контролируются индекс цикла I ) вы можете иметь раздел кода , как:

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

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

Это все VBA (в отличие от других ответов)

Функция, которая выполняет функцию суб / асинхронно

Модуль формы, необходимой для достижения этой цели

(Имя формы = MultiThreadingEngine, не имеет какой-либо контроля или свойства, установленный)

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

Я искал что-то подобное, и официальный ответ нет. Тем не менее, мне удалось найти интересную концепцию Даниила в ExcelHero.com.

В принципе, вам нужно создать рабочие VBScripts для выполнения различных вещей, которые вы хотите и он доложит, чтобы преуспеть. За то, что я делаю, извлечение данных HTML из различных веб-сайта, он прекрасно работает!

Как вы, вероятно, узнали VBA изначально не поддерживает многопоточность, но. Есть 3 способа для достижения многопоточности:

  1. COM / DLL , — например , C # и параллельный класс для запуска в отдельных потоках
  2. Использование VBScript рабочих потоков — запустить свой код VBA в отдельных потоках VBScript
  3. Использование VBA рабочих потоков , выполненных , например , с помощью VBscript — скопировать книгу Excel и запустить макрос параллельно.

Учитывая подход # 3 я также сделал инструмент VBA Многопоточность , что позволяет легко добавлять многопоточность VBA: http://analystcave.com/excel-vba-multithreading-tool/

Смотрите примеры ниже:

Многопоточность петелька для

Запуск макроса асинхронно Excel

Многопотоковость в visual basic

Компонент BackgroundWorker заменяет аналогичный код из пространства имен System.Threading и расширяет его функциональные возможности; однако при необходимости исходное пространство имен System.Threading можно сохранить для обеспечения обратной совместимости и использования в будущем. Дополнительные сведения см. в разделе Общие сведения о компоненте BackgroundWorker .

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

Приложение будет состоять из одной формы и компонента. Пользователь будет вводить значения и сообщать компоненту о необходимости начать вычисления. Форма будет получать из компонента значения и отображать их в элементах управления «Label». Компонент будет выполнять вычисления, занимающие процессор, и сообщать форме о завершении. Для хранения значений, полученных из интерфейса пользователя, в компоненте следует создать общие переменные. В компоненте следует также реализовать методы для выполнения вычислений на основе значений этих переменных.

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

Отображаемые диалоговые окна и команды меню могут отличаться от описанных в справке в зависимости от текущих настроек или выпуска. Чтобы изменить параметры, в меню Сервис выберите команду Импорт и экспорт параметров . Дополнительные сведения см. в разделе Настройка параметров разработки в Visual Studio .

Чтобы создать форму, выполните следующие действия.

Создать новый проект Приложение Windows .

Задайте для приложения имя Calculations и переименуйте форму Form1.vb в frmCalculations.vb .

Получив приглашение от Visual Studio переименовать элемент кода Form1 , нажмите кнопку Да .

Эта форма будет служить в приложении основным интерфейсом пользователя.

Добавьте в форму пять элементов управления Label, четыре элемента управления Button и один элемент управления TextBox.

Иллюстрированный самоучитель по Visual Basic .NET

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Область видимости в Visual Basic Scope in Visual Basic

Областью видимости объявленного элемента является набор всего кода, который может ссылаться на него без уточнения его имени или предоставления доступа с помощью оператора Imports (пространство имен .NET и тип). The scope of a declared element is the set of all code that can refer to it without qualifying its name or making it available through an Imports Statement (.NET Namespace and Type). Элемент может иметь область действия на одном из следующих уровней: An element can have scope at one of the following levels:

Примечание
Уровень Level Описание Description
Область действия блока Block scope Доступно только в блоке кода, в котором он объявлен Available only within the code block in which it is declared
Область действия процедуры Procedure scope Доступен для всего кода в процедуре, в которой он объявлен Available to all code within the procedure in which it is declared
Область модуля Module scope Доступен для всего кода в модуле, классе или структуре, в которой он объявлен Available to all code within the module, class, or structure in which it is declared
Область пространства имен Namespace scope Доступен для всего кода в пространстве имен, в котором оно объявлено Available to all code in the namespace in which it is declared

Эти уровни области выполняются от самого узкого (блока) до самого широкого (пространства имен), где самая узкие области означает наименьший набор кода, который может ссылаться на элемент без квалификации. These levels of scope progress from the narrowest (block) to the widest (namespace), where narrowest scope means the smallest set of code that can refer to the element without qualification. Дополнительные сведения см. в разделе уровни области на этой странице. For more information, see «Levels of Scope» on this page.

Указание области и определение переменных Specifying Scope and Defining Variables

Вы указываете область элемента при его объявлении. You specify the scope of an element when you declare it. Область может зависеть от следующих факторов: The scope can depend on the following factors:

Область (блок, процедура, модуль, класс или структура), в которой объявляется элемент The region (block, procedure, module, class, or structure) in which you declare the element

Пространство имен, содержащее объявление элемента The namespace containing the element’s declaration

Уровень доступа, объявленный для элемента The access level you declare for the element

Используйте осторожность при определении переменных с одинаковым именем, но с разными областями, так как это может привести к непредвиденным результатам. Use care when you define variables with the same name but different scope, because doing so can lead to unexpected results. Для получения дополнительной информации см. References to Declared Elements. For more information, see References to Declared Elements.

Уровни области Levels of Scope

Программный элемент доступен по всему региону, в котором он объявлен. A programming element is available throughout the region in which you declare it. Весь код в одном регионе может ссылаться на элемент без уточнения его имени. All code in the same region can refer to the element without qualifying its name.

Область блока Block Scope

Блок — это набор инструкций, заключенных в операторы инициирования и завершения объявления, как показано ниже: A block is a set of statements enclosed within initiating and terminating declaration statements, such as the following:

Do и Loop Do and Loop

For [ Each ] и Next For [ Each ] and Next

If и End If If and End If

Select и End Select Select and End Select

SyncLock и End SyncLock SyncLock and End SyncLock

Try и End Try Try and End Try

While и End While While and End While

With и End With With and End With

Если переменная объявлена в блоке, ее можно использовать только в пределах этого блока. If you declare a variable within a block, you can use it only within that block. В cube следующем примере областью целочисленной переменной является блок между If и End If , и вы больше не можете ссылаться на cube то, когда выполнение передается из блока. In the following example, the scope of the integer variable cube is the block between If and End If , and you can no longer refer to cube when execution passes out of the block.

Даже если область действия переменной ограничена блоком, ее время существования все равно будет содержаться во всей процедуре. Even if the scope of a variable is limited to a block, its lifetime is still that of the entire procedure. Если вы вводите блок несколько раз во время процедуры, каждая переменная блока удерживает свое предыдущее значение. If you enter the block more than once during the procedure, each block variable retains its previous value. Чтобы избежать непредвиденных результатов в таком случае, разумно инициализировать блочные переменные в начале блока. To avoid unexpected results in such a case, it is wise to initialize block variables at the beginning of the block.

Область действия процедуры Procedure Scope

Элемент, объявленный внутри процедуры, недоступен за пределами этой процедуры. An element declared within a procedure is not available outside that procedure. Только процедура, содержащая объявление, может использовать его. Only the procedure that contains the declaration can use it. Переменные на этом уровне также называются локальными переменными. Variables at this level are also known as local variables. Они объявляются с помощью оператора Dimс ключевым словом static или без него. You declare them with the Dim Statement, with or without the Static keyword.

Область действия процедуры и блока тесно связана. Procedure and block scope are closely related. При объявлении переменной внутри процедуры, но вне любого блока внутри этой процедуры можно считать, что переменная имеет область видимости блока, где блоком является вся процедура. If you declare a variable inside a procedure but outside any block within that procedure, you can think of the variable as having block scope, where the block is the entire procedure.

Все локальные элементы, даже если они являются Static переменными, являются частными для процедуры, в которой они отображаются. All local elements, even if they are Static variables, are private to the procedure in which they appear. Нельзя объявить какой-либо элемент с помощью ключевого слова Public в процедуре. You cannot declare any element using the Public keyword within a procedure.

Область модуля Module Scope

Для удобства уровень модуля единого термина применяется в равной степени к модулям, классам и структурам. For convenience, the single term module level applies equally to modules, classes, and structures. Вы можете объявить элементы на этом уровне, поместив оператор объявления за пределы любой процедуры или блока, но внутри модуля, класса или структуры. You can declare elements at this level by placing the declaration statement outside of any procedure or block but within the module, class, or structure.

При создании объявления на уровне модуля этот уровень доступа определяет область. When you make a declaration at the module level, the access level you choose determines the scope. Пространство имен, содержащее модуль, класс или структуру, также влияет на область. The namespace that contains the module, class, or structure also affects the scope.

Элементы, для которых объявлен частный уровень доступа, доступны для каждой процедуры в этом модуле, но не для кода в другом модуле. Elements for which you declare Private access level are available to every procedure in that module, but not to any code in a different module. Инструкция на уровне модуля по умолчанию принимает Private значение, если не используются ключевые слова уровня доступа. Dim The Dim statement at module level defaults to Private if you do not use any access level keywords. Тем не менее можно сделать область и уровень доступа более очевидными с помощью Private ключевого слова Dim в инструкции. However, you can make the scope and access level more obvious by using the Private keyword in the Dim statement.

В следующем примере все процедуры, определенные в модуле, могут ссылаться на строковую переменную strMsg . In the following example, all procedures defined in the module can refer to the string variable strMsg . При вызове второй процедуры она отображает содержимое строковой переменной strMsg в диалоговом окне. When the second procedure is called, it displays the contents of the string variable strMsg in a dialog box.

Область пространства имен Namespace Scope

Если элемент объявляется на уровне модуля с помощью ключевого слова Friend или Public , он становится доступным для всех процедур в пространстве имен, в котором объявлен элемент. If you declare an element at module level using the Friend or Public keyword, it becomes available to all procedures throughout the namespace in which the element is declared. После выполнения следующей изменения в предыдущем примере строковая переменная strMsg может называться кодом в любом месте пространства имен его объявления. With the following alteration to the preceding example, the string variable strMsg can be referred to by code anywhere in the namespace of its declaration.

Область пространства имен включает вложенные пространства имен. Namespace scope includes nested namespaces. Элемент, доступный из пространства имен, также доступен из любого пространства имен, вложенного в это пространство имен. An element available from within a namespace is also available from within any namespace nested inside that namespace.

Если проект не содержит операторов пространства имен, все в проекте находится в том же пространстве имен. If your project does not contain any Namespace Statements, everything in the project is in the same namespace. В этом случае область пространства имен может рассматриваться как область проекта. In this case, namespace scope can be thought of as project scope. Public элементы в модуле, классе или структуре также доступны для любого проекта, который ссылается на свой проект. Public elements in a module, class, or structure are also available to any project that references their project.

Выбора области Choice of Scope

При определении переменной следует учитывать следующие моменты при выборе ее области действия. When you declare a variable, you should keep in mind the following points when choosing its scope.

Преимущества локальных переменных Advantages of Local Variables

Локальные переменные являются хорошим выбором для любого временного вычисления по следующим причинам. Local variables are a good choice for any kind of temporary calculation, for the following reasons:

Конфликт имен — избежать. Name Conflict Avoidance. Имена локальных переменных не подвержены конфликтам. Local variable names are not susceptible to conflict. Например, можно создать несколько различных процедур, содержащих переменную с именем intTemp . For example, you can create several different procedures containing a variable called intTemp . Если каждая из intTemp них intTemp объявлена как локальная переменная, каждая процедура распознает только собственную версию. As long as each intTemp is declared as a local variable, each procedure recognizes only its own version of intTemp . Любая процедура может изменить значение в локальной intTemp переменной, не влияя intTemp на переменные в других процедурах. Any one procedure can alter the value in its local intTemp without affecting intTemp variables in other procedures.

Потребление памяти. Memory Consumption. Локальные переменные потребляют память только во время выполнения процедуры. Local variables consume memory only while their procedure is running. Их память освобождается, когда процедура возвращается в вызывающий код. Their memory is released when the procedure returns to the calling code. В отличие от этого, Общие и статические переменные потребляют ресурсы памяти до тех пор, пока приложение не прекратит работу, поэтому используйте их только при необходимости. By contrast, Shared and Static variables consume memory resources until your application stops running, so use them only when necessary. Переменные экземпляра потребляют память, пока их экземпляры продолжают существовать, что делает их менее эффективными, чем локальные переменные, но потенциально Shared более Static эффективными, чем переменные. Instance variables consume memory while their instance continues to exist, which makes them less efficient than local variables, but potentially more efficient than Shared or Static variables.

Минимизация области Minimizing Scope

Как правило, при объявлении любой переменной или константы рекомендуется сделать область как можно более узким (область блока является самой узким). In general, when declaring any variable or constant, it is good programming practice to make the scope as narrow as possible (block scope is the narrowest). Это помогает экономить память и минимизирует вероятность того, что код ошибочно ссылался на неверную переменную. This helps conserve memory and minimizes the chances of your code erroneously referring to the wrong variable. Аналогично, следует объявить переменную как статическую , только если необходимо сохранить ее значение между вызовами процедур. Similarly, you should declare a variable to be Static only when it is necessary to preserve its value between procedure calls.

Илон Маск рекомендует:  Что такое код ibase_query
Понравилась статья? Поделиться с друзьями:
Кодинг, CSS и SQL