На тему многопоточности мы говорили уже немало, однако, как я уже неоднократно упоминал, это не значит, что о ней нельзя ничего сказать ещё. Но всё-таки газета есть газета, и рассказывать на страницах "Компьютерных вестей" исключительно и только о многопоточных приложениях нельзя. Так что эта статья будет итоговой.
Мы, вроде бы, поговорили уже практически обо всём, что стоило нашего внимания, но при этом позволяло не особенно углубляться в детали. Мы с вами изучили, чем семафоры отличаются от критических секций и чем хороши критические секции, по сравнению с семафорами. Мы рассмотрели вопросы синхронизации двух разных потоков и узнали, что Synchronize в Delphi и synchronized в Java - это, как говорится, две очень даже большие разницы. О чём же ещё можно поговорить, если кажется, что просто обсуждать общие для Windows-программирования принципы и при этом не вдаваться в тонкости конкретного языка программирования уже не получится? Оказывается, поговорить очень даже есть о чём. И сейчас мы перейдём к теме, которая будет важна при создании приложений, где разные потоки играют разные по степени своей значимости роли. То есть, фактически, один поток является главнее другого. Мы поговорим с вами о приоритетах потоков.
Приоритеты процессов
Думаю, вдаваться в объяснения смысла термина "приоритеты" точно не нужно. Это слово знакомо всем с детства: "приоритеты внешней политики", "приоритеты в развитии бизнеса", "личные приоритеты"... Теперь ещё и приоритеты потоков. Слово "приоритеты" здесь нужно понимать в его классическом, первозданном, виде. В отличие от множества других терминов, этот не подвергся сколько-нибудь существенному изменению применительно к информационным технологиям.
Каким образом реализуется существование различных приоритетов у различных потоков? Дело в том, что потоками ведает операционная система, а потому именно она ответственна за выделения процессорного времени каждому из потоков. Принцип простой: чем более важен поток, то есть, чем более высокий у него приоритет, тем большего количества процессорного времени он заслуживает. Общий приоритет того или иного потока зависит от приоритета того процесса, в котором запущен поток, и от относительного приоритета потока среди всех потоков, работающих в рамках данного конкретного процесса.
Приоритеты процессов, с которыми при программировании обычных пользовательских приложений в Delphi дело иметь приходится весьма и весьма редко, часто называются классами приоритетов, в Windows бывают пяти видов: Idle, Below Normal, Normal, Above Normal, High и Realtime. Расположены они в приведённом списке в порядке возрастания. Первый, Idle, означает, что система будет отдавать время этому процессу только тогда, когда ей просто нечем будет больше заняться. Последний, Realtime, подразумевает, что приложение работает в режиме реального времени (например, получая данные с каких-то внешних приборов и, исходя из их значений, управляя ядерным реактором), а потому система должна лечь костьми, чтобы только приложение работало. Все промежуточные классы приоритета, соответственно, подразумевают менее экстремальные случаи. При этом стоит отметить, что классы Below Normal и Above Normal появились только в Windows 2000, поэтому использовать их в приложениях, которые, по замыслу разработчика, должны уметь правильно работать и на более старых операционных системах линейки Windows, скажем прямо, нежелательно.
Классам приоритета соответствуют константы, которые именуются как XXX_PRIORITY_CLASS, где вместо XXX стоит, соответственно, имя того класса приоритета, который вы будете использовать. Для Below Normal и Above Normal нужно использовать подчёркивание в названии между словом Normal и приставкой; Realtime пишется в одно слово. Константы эти в случае Delphi находятся в модуле Windows.
По умолчанию каждому процессу присваивается класс приоритета Normal, а для его изменения нужно либо указать соответствующий класс приоритета в функции CreateProcess, либо воспользоваться функцией, изменяющей приоритет уже работающего приложения. Функция эта называется SetPriorityClass и имеет два параметра. Первый - это дескриптор того процесса, приоритет которого нужно поменять (для получения дескриптора текущего процесса имеет смысл воспользоваться функцией GetCurrentProcess), а второй - класс приоритета, который данный процесс должен получить.
Впрочем, стоит отметить, что пользоваться данной функцией нужно довольно осторожно, и лучше тщательно взвесить все "за" и "против", прежде чем прибегать к изменению приоритета процесса. Дело в том, что процессы с приоритетом выше нормального запросто способны заблокировать работу всех остальных процессов, а потому их и рекомендуется применять только в тех приложениях, где жизненно необходимо обеспечить высокую степень доступности в любой момент времени.
Приоритеты потоков
Поскольку потоки, работающие в рамках одного процесса, могут быть неоднородны, существует также относительный приоритет потоков в рамках данного процесса. В рамках многопоточного приложения к его изменению прибегают чаще, чем к изменению общего приоритета процесса, поскольку даже в приложениях, работающих с нормальным классом приоритета, есть потоки основные и вспомогательные. Классический пример - всё та же проверка орфографии. Очевидно, что она менее важна, чем, скажем, поток, сохраняющий текст в базу данных, а потому её относительный приоритет имеет смысл сделать более низким, чем относительный приоритет сохраняющего информацию потока.
Относительные приоритеты потоков могут быть следующими: Idle, Below Normal, Normal, Above Normal, Highest и Time Critical. Здесь, как видите, названия в "топовой категории" списка несколько поменялись, но суть, в принципе, всё та же. В Delphi для изменения относительного приоритета потока не потребуется никаких функций Windows API, потому что у класса TThread, наследником которого будет наш класс потока, есть специально придуманное для этого случая свойство Priority. Оно может принимать значения tpXXX, где XXX - это, соответственно, относительный приоритет потока. Здесь уже, в отличие от констант Windows API, всё пишется слитно и никаких подчёркиваний не нужно.
Стоит помнить, что относительный приоритет потока на то и относительный, что влияет только на работу потока в рамках данного отдельно взятого многопоточного приложения. И если вам нужно, чтобы приоритет потока был более высоким в рамках всех запущенных в системе потоков, придётся поколдовать и над классом приоритета всего приложения. Что касается выставления высоких относительных приоритетов потоков, то здесь можно дать тот же самый совет, который касался класса приоритета процесса. Если вы не хотите, чтобы постоянно активный поток блокировал работу всего приложения, не присваивайте высокий приоритет такому потоку.
Переменные в потоках
Сейчас я всё-таки расскажу об одном специфическом для Delphi механизме, связанном с хранением данных в глобальных переменных для разных потоков.
Итак, о чём, собственно говоря, идёт речь. В многопоточном приложении, как и в любом другом, есть глобальные переменные, а есть локальные. Локальные для каждого потока свои, даже когда у нас несколько экземпляров одного и того же класса потока - это, в общем-то, вполне естественно. Взаимодействие различных потоков с глобальными переменными также вполне объяснимо: каждый поток изменяет глобальные переменные таким образом, что следующий поток имеет дело уже с новым значением переменной. Всё очень просто и логично. Однако иногда, в процессе спешной переделки однопоточного приложения в многопоточное, неожиданно обнаруживается, что поменять эту глобальную переменную на локальную означает переписать, как минимум, третью часть всего кода приложения, что, мягко говоря, не слишком приятно. Что же делать в таком случае? Прибегнуть к помощи глобальных поточных переменных - threadvar'ов.
Если мы воспользуемся для объявления переменной не ключевым словом var, а threadvar, то тем самым мы покажем компилятору, что данную переменную нужно рассматривать как зависящую от потока. Если мы обратимся из одного потока к данной переменной, которая как бы является глобальной, то получим одно её значение, а если затем обратимся к ней же, но уже из другого потока, то значение будет совершенно иным. Казалось бы, это довольно удобно, но, на самом деле, здесь есть некоторые подводные камни. Например, если мы воспользуемся этой переменной в функции, которая вызывается через использование метода Synchronize, то мы, на самом деле, не получим того значения, которое записано для данного потока (и которое мы, скажем, собирались вывести на экран). Так что использовать такие глобальные переменные, которые на самом деле нельзя назвать полностью глобальными, нужно очень и очень аккуратно. Хотя, как я уже говорил, очень часто они бывают более чем удобными, а потому иметь в виду факт их существования при написании многопоточных приложений всё-таки нужно.
Резюме
Что ж, давайте подведём итоги нашему начавшемуся ещё в прошлом году разговору о многопоточных приложениях. Вполне возможно, он получился несколько затянутым, и некоторые моменты я освещал излишне подробно. Что ж, если это так, приношу свои извинения - просто я старался сделать статьи понятными даже не самому хорошо подготовленному к написанию многопоточных приложений человеку.
С одной стороны, после прочтения всех этих материалов может создаться впечатление, что написание многопоточных приложений - занятие простое и особого напряжения ума и воли не требующее. В общем-то, поскольку все приложения, которые мы рассматривали в рамках этой серии статей, были довольно простыми, то такое мнение имеет право на существование. Однако, на самом деле, многопоточные приложения, особенно когда в них много мест, требующих синхронизации, - это не просто. Впрочем, как говорится, дорогу осилит идущий, и каждая отлаженная и устранённая собственными силами ошибка - это бесценный вклад в опыт, который неоднократно пригодится в будущем.
В любом случае, думаю, статьи эти были вам полезны. Если даже вы не узнали ничего нового о создании многопоточных приложений, то наверняка освежили в памяти эту тему, что тоже, в общем-то, не вредно. Если у вас есть что добавить или вы хотели что-либо узнать, то милости прошу, пишите на мой почтовый ящик или на форум газеты. Хочу также поблагодарить Игоря Щербича за содействие в подготовке некоторых материалов из серии про многопоточные приложения.
P.S. Напоследок хочу порекомендовать в продолжении темы опубликованную недавно на DelphiPlus.org статью Андрея Боровского "Многопоточные программы в Delphi изнутри" (delphiplus.org/articles/delphi/multi-threads_programs.html).
Вадим СТАНКЕВИЧ,
[email protected]
Горячие темы