    Работа с подзадачами 

    ГЛАВА 24 

    Джулиан Букнел 

    Подзадачи—один из наиболее мощных инструментов, который поступает в ваше 
распоряжение при программировании для Windows 95 и Windows NT. Программисты 
ломают над подзадачами головы (обычно считая их очень сложными для 
использования) и либо опасаются их применять, либо пытаются вообще забыть об 
их существовании. Подзадачи в полной мере эксплуатируют операционную систему, 
и их использование характерно для большинства приложений 32-разрядной версии 
Windows. К примеру, летящие листы бумаги, которые вы видите, когда копируете 
файл в Windows 95 или Windows NT 4.0, отображаются на экране благодаря 
активной подзадаче, в то время как основной процесс занят копированием данных. 
    Подзадачи позволяют приложениям выполнять сразу несколько операций 
(по^крайней мере, они создают иллюзию одновременного выполнения нескольких 
операций). Если вы не входите в число счастливчиков, имеющих 
мультипроцессорную систему, значит, в вашем PC находится один центральный 
процессор и он может в один момент времени выполнять только одну задачу, т.е. 
вы ограничены последовательной архитектурой процессоров, разработанной фон 
Нейманом. Тем не менее многие процессоры работают довольно быстро и могут 
создавать видимость одновременного выполнения нескольких задач, используя 
чудесные программы-переключатели в 32-разрядной операционной системе. На самом 
деле операционная система героически разрывается между различными процессами и 
подзадачами, приостанавливая выполняемую задачу и переходя к следующей в вечно 
крутящейся карусели. 
    Основная цель этой главы — предоставить вам информацию о подзадачах и их 
использовании и дать вам прочувствовать прелесть подзадач и сопровождающего их 
окружения. Честно говоря, возможно, нет ничего более увлекательного, чем 
испытания вашего первого приложения с подзадачами (безусловно, я допускаю, что 
существуют и кое-какие другие забавные вещи, но подзадачи — интереснее всего! 
Это уж точно!). К несчастью, природа всего лучшего, что есть в этой главе, в 
основном техническая и местами глава может показаться слиш-| 

    Терминология 

    Понимание терминологии, принятой длягподзадач, жизненно необходимо для 
понимания того, о чем говорится в этой главе. 

    Тематика 

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

    Объекты синхронизации 

    Здесь рассматриваются различные объекты синхронизации, которые применяет 
Windows для обеспечения совместной работы подзадач. Поработаем с событиями, 
мьютексами и семафорами! 

    Локальная память подзадач 

    Вы узнаете, как писать программы с подзадачами таким образом, чтобы они 
имели доступ к глобальным данным. 

    Класс Delphi TThread 

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

560

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

    Терминология, принятая для подзадач 

    Нравится вам это или нет, но мы немедленно должны определить некоторые 
термины и получить представление об их использовании. Вы пожалеете потом, если 
проигнорируете это сейчас. 
    Первым термином является Win32. Он будет использоваться много раз. Под 
Win32 я подразумеваю или Windows 95, или Windows NT (или и то, и другое 
сразу), 32-разрядную операционную систему Windows (я не буду здесь спорить, 
правильно ли называть Windows 95 32-разрядной операционной системой, потому 
что в ней до сих пор применяются 16-разрядные библиотеки DLL). Если я буду 
обращаться к чему-то специфическому для одной или другой операционной системы, 
я оговорю это отдельно. И еще: обращаясь к подпрограмме, как 
№1п32-подпрограмме (Win32 routine), я имею в _виду, что она декларирована в 
модуле Delphi Windows (исходный текст — WINDOWS . PAS). 
    Следующий термин — процесс. Это приложение или программа, которую Win32 
загружает в память. Win32 также создает для приложения пространство 
виртуальных адресов. Некоторые авторы используют в этом же смысле слова задача 
(task), приложение (application) и программа (programm), однако Win32 SDK 
Microsoft (Software Development Kit) обычно использует слово процесс. 
    И последний термин — подзадача (thread; для обозначения подзадач в 
русскоязычной литературе используются также термины минизадача, цепочка, 
поток, нить. — Прим. перев.). Это, собственно, и есть материал данной главы. 
Подзадача представляет собой объект Win32. Когда Win32 создает новый процесс 
(загружаемое приложение), для него создается подзадача, чтобы запустить 
программный код. Такие подзадачи называются первичными. Любая подзадача может 
создавать одну или несколько подзадач для одного и того же процесса, и все эти 
подзадачи используют одно и то же пространство виртуальных адресов. Такое 
пространство создается процессом, сформировать который и легко, и сложно, как 
вы увидите дальше. Его цель -— разделить данные между подзадачами. Каждая 
подзадача может выполнять один и тот же код, либо отдельные подзадачи могут 
выполнять отдельные фрагменты кода. Когда заканчивается выполнение первичных 
подзадач, процесс выгружается из системы и приложение завершается. 
    Повторю еще раз: подзадачи, а не процессы, отвечают за выполнение кода. 
Процессы определяют данные, ресурсы и адресное пространствоГкоторые будут 
использоваться подзадачами, работающими в этом процессе. 

    Обзор подзадач 

    Ясно понимать силу подзадач начинаешь только тогда, когда осознаешь, что 
первичная подзадача может создавать другие подзадачи, а они, в свою очередь, 
могут создавать следующие и т.д. Все подзадачи процесса выполняются 
одновременно. Win32 автоматически переключаются между подзадачами в процессе, 
не требуя для этого каких-либо ваших действий или управления переключениями. 
Это важное положение мы раскроем немного позже. Действительно, на некоторых 
многопроцессорных машинах различные подзадачи будут осуществляться в одно и то 
же время, но на разных физических центральных процессорах. Обычно, однако, 
ваше приложение будет работать на однопроцессорном компьютере, и в этом случае 
Win32 будет организовывать режим вытесняющей многозадачности для ваших 
подзадач, чтобы они получили равное количество процессорного времени. (Замечу 
в скобках, что последнее утверждение сильно упрощено, так как количество 
процессорного времени для каждой подзадачи определяется различными факторами, 
такими как приори тет подзадачи, ожидает ли подзадача сигнала 
рт_объекта^1нхр.онизации и т.д. Мы вернемся к этому чуть позже в этой главе.) 
    "Каждая подзадача выполняется во всем пространстве памяти процесса. Таким 
образом, каждая подзадача может видеть те же глобальные данные, что и все 
остальные подзадачи в этом приложении. Все эти подзадачи также могут менять 
одни и те же глобальные данные в этом процессе, и здесь и кроется основная 
проблема подзадач — условие так называемого состязания. Представьте себе, что 
подзадачи А и Б собираются вносить изменения в глобальные переменные, а затем 
использовать их в вычислениях. Подзадача А выполняется и увеличивает значение 
переменной. Пока это все продолжается, Win32 проделывает фокус с 
многозадачностью и подзадача А погружается в сон, а подзадача Б начинает 
выполняться. Она увеличивает значение той же глобальной переменной, а потом 
заканчивает свои расчеты. Один взмах волшебной многозадачной палочки — и опять 
просыпается подзадача А. Она начинает свои расчеты, но с неправильным 
значением глобальной переменной. Позже в этом разделе будут рассмотрены 
локальные переменные подзадачи и объекты синхронизации Win32, которые 
понадобятся для того, чтобы изменять и читать глобальные переменные и 
структуры без влияния на них вытесняющей многозадачности. 
    Предположим, что вы являетесь гуру в написании подзадач. Где в таком 
случае можно использовать подзадачи в приложении? Вы можете добавить подзадачи 
куда угодно и писать подпрограмму синхронизации, пытаясь заставить их работать 
вместе, а на Win32 возложить заботу о результате. Однако я гарантирую вам, что 
результат будет представлять собой 

    Глава 24. Работа с подзадачами 561 

эдакую версию спагетти в четвертом измерении. (Кстати о спагетти. Моя 
приятельница швыряла спагеттину о стену и таким образом проверяла, готово ли 
спагетти: если та прилипала, значит, сварилась. Программа с безалаберной 
организацией подзадач будет весьма похожа на эту "тестовую" стену...) Поставим 
наш вопрос с головы на ноги: вы не должны писать код подзадачи только из-за 
того, что знаете, как это делается; пишите код подзадачи, потому что без нее 
пользователя будет раздражать кажущаяся медлительность вашего приложения. Все 
же слишком часто мы как программисты забываем, что пишем приложения для 
конечного пользователя. Единственным критерием, по которому наше приложение 
будет оцениваться, — это его чувствительность к таким действиям пользователя, 
как нажатие клавиши на клавиатуре или щелчок кнопкой мыши. 
    Другое соображение: предположим, что вы имеете какое-то среднее количество 
операций, которые выполняются в программе, и желаете знать, можно ли с пользой 
для себя использовать подзадачи для "ускорения". Если операция не предполагает 
какого-нибудь ввода-вывода, то в общем случае сделать программу более быстрой, 
разделив ее на подзада-^чм,"нельзя." 
    Предположим, что программа использует быструю сортировку по порядку 
находящегося в памяти массива. В эТбм"случае разбивка этой быстрой сортировки 
на сортировки по частям в различных подзадачах делает программу в целом более 
медленной. Вы просто несете дополнительные расходы на переключение между 
подзадачами во время выполнения вашей быстрой сортировки. А теперь 
предположим, что работа программы состоит в сортировке большого файла. В этом 
случае вполне возможно, что наличие нескольких подзадач, обеспечивающих 
быструю сортировку отдельных фрагментов файла, может стать ускорителем 
программы, так как пока одна подзадача ожидает следующего блока, который 
считывдет,с,я..и.з файлаДдлительная операция), другая подзадача может 
сортировать уже прочитанный блок. 
    Теперь предположим, что вы создаете редактор с проверкой правописания. Вы 
хотите проверить правописание каждого слова, которое набрал пользователь, и 
выделить его в том случае, если оно написано неверно (почти так, как делает 
Word 97). Вы можете проверять правописание пользователя в вашей главной 
программе, но, возможно, это будет слишком долго для пользователей, набирающих 
текст со средней и высокой скоростью. Пользователь может почувствовать, что 
редактор впадает в спячку после каждого слова. И здесь подзадача могла бы 
выступить во всей красе: она должна только проверять правописание слов. 
Главная подзадача должна бы отвечать за управление показанным на дисплее 
текстом, перевод ударов по клавишам в буквы и т.п. Win32 заботится о 
многозадачности, а вы можете жонглировать приоритетами подзадачи переднего 
плана и подзадачи, которая проверяет правописание. 
    Рассмотрим другой пример. Вы пишете статистическое приложение, которое 
считывает последовательности данных, а затем выполняет некоторые существенные 
преобразования. Эти вычисления могут занять весьма длительное время, а вы 
хотите показывать пользователю простейшие результаты по ходу расчетов. Можно 
также показывать и сами данные и позволить пользователю вносить в них 
изменения по принципу "А что если посмотреть вот это?". Это имеет смысл для 
расчетов, которые осуществляются в фоновой подзадаче, и она могла бы указывать 
главной подзадаче на изменение каких-то результатов. Основная подзадача, в 
свою очередь, могла бы сообщать расчетной подзадаче о том, что данные 
изменились и что она должна прервать свои текущие вычисления и начать сначала. 
Пользователю будет казаться, что программа быстро реагирует на его действия, 
так как од де должен ожидать завершения каждого вычисления, прежде чем 
прокручивать следующий сценарий "а что если". Подзадачи упрощают написание 
кода такого рода благодаря тому, что каждая новая подзадача имеет четко 
определенную цель. Операционная система отвечает за переключение между 
подзадачами и за то, чтобы все сделанное в целом работало гладко. 
    1  Другая точка зрения на подзадачи. Несмотря на то что операционные 
системы Win32 — знатоки в области задач переключения, на это все-таки 
требуется некоторое время. Обычно Windows NT лучше справляется с переключением 
задач, чем Windows 95. Таким образом, если вы перегружаете свое приложение 
подзадачами, которые все работают в одно и то же время, для пользователя 
основной процесс может ползти, как улитка, из-за того что значительное время 
уходит на переключение между подзадачами, а не на выполнение кодов приложения. 

    Как запустить и остановить подзадачу 

    Ну а теперь о том, как запустить подзадачу и остановить ее выполнение. На 
самом нижнем уровне подзадачи создаются путем вызова стандартной подпрограммы 
Win32 CreateThread. В Delphi, однако, вызов этой подпрограммы напрямую — 
сложное дело для программистов, потому что для приложений Delphi CreateThread 
является уж слишком низким уровнем, который надо вызывать. Вызова этой функции 
напрямую недостаточно для того, чтобы библиотека рабочих процедур (runtime 
library) увидела новую подзадачу. Здесь существует две проблемы. Во-первых, 
когда вы напрямую вызываете CreateThread, глобальная переменная с именем 
IsMultiThread (она находится в модуле System) H? устанавливается равной True. 
Менеджер кучи (heap manager) в Delphi использует эту переменную, чтобы узнать, 
когда в процессе работают множественные""Подзадачи; зная это, он может защищать 
внутренние структуры от обновления сразу несколькими подзадачами. И, во-вторых, 
BeginThread обеспечивает кадр исключения для новой подзадачи, тогда 
как CreateThread не делает этого.	.— •--..... 

    562	Часть VI. Сложный материал 


    ПРЕДОСТЕРЕЖЕНИЕ 
    Никогда не используйте непосредственно функцию createThread для создания 
подзадач. Она не обеспечивает безопасности новой подзадачи, насколько это 
необходимо библиотеке рабочих процедур Delphi. 

    Модуль System предоставляет замену функции CreateThread, называемую 
BeginThread. Вы должны использовать именно эту функцию.	* "'"" • .-• . .-————•— 

    function BeginThread   (SecurityAttributes:   Pointer; 
    StackSize;   Integer; 
    ThreadFunc:   TThreadFunc; 
    Parameter   :   Pointer; 
    CreationFlags   :   Integer; 
    varThreadld   :   Integer)    :   Integer; 

    SecurityAttributes — это запись для определения прав доступа новой 
подзадачи (ее определение и использование выходит за рамки этой главы; 
обратитесь к справочному файлу Win32). StackSize — полный размер стека для 
новой подзадачи; если вы здесь задаете 0, то система использует установку 
Delphi по умолчанию, равную 1 Мбайт. ThreadFunc — это функция 
    Delphi, которую будет применять новая подзадача. Она имеет тип TThreadFunc: 

    TThreadFunc =  function(Parameter   :   Pointer)    :   Integer; 

    Параметр Parameter— это величина, которая передается как параметр в 
функцию BeginThread. Обычно это или Nil (вы не хотите передавать подзадаче 
никаких данных), или указатель на запись (вы хотите передавать данные 
подзадаче). Вы также можете конвертировать тип long integer в pointer и таким 
образом передать подзадаче числовое значение. 
    Невозможно инициализировать структуру данных, вызвать BeginThread, 
передавая ей адрес этой структу-Совет 1 Рь| в качестве параметра, изменить эту 
структуру и вновь вызвать BeginThread для другой подзадачи. Функция подзадачи 
делапускается^сг^азу после вызащJjJLgaJLnJ-bxfiad» и такое "быстрое" 
копирование в начале функции не сможет вам помочь. Вы должны распределять 
отдельные копии данных для каждой подзадачи и передавать соответствующие 
данные каждому вызову BeginThread. 
    Вернемся вновь к параметрам BeginThread. CreationFlags определяет 
состояние, из которого будет запускаться подзадача. Если это состояние 0, то 
запуск осуществляется немедленно (или, вернее, когда система получит время, 
чтобы можно было сделать это немедленно). Если это условие CREATE_SUSPENDED, 
то подзадача создается и приостанавливается, и ни один байт ее кода не 
выполняется до тех пор, пока вы не вызовете подпрограмму Win32 ResumeThread. 
Параметр Threadld — это численный идентификатор (он уникален в пределах 
системы) новой подзадачи. Он возвращается после завершения вызова BeginThread. 
    Результатом выполнения функции BeginThread является дескриптор подзадачи 
(если он равен 0, произошла ошибка и вы должны вызывать стандартную 
подпрограмму, ЖшЗ_2_ Ge. t La.s tEr г о г, чтобы[обнаружить причину этой 
ошибки). Обращаю ваше внимание на то, что результат выполнения функции 
BeginThread — это не результат функции подзадачи! Подзадача не принимала 
участия в этом деле. Единственное, что произошло благодаря ей,,— создание и 
подготовка ядра подзадач и. 
    С другой стороны, результат выполнения функции подзадачи является ее кодом 
выхода. Вы можете запросить его, используя подпрограмму Win32 GetExitCodeThread: 

    function GetExitCodeThread(hThread:   THandle; 
    var   IpExitCode:   DWORD)    :   BOOL; 

    Подзадача завершает работу либо по окончании выполнения функции подзадачи 
(функция подзадачи выполняется до конечного оператора end;), либо при вызове 
подпрограммы Win32 ExitThread. Однако, чтобы заставить подзадачу прервать 
выполнение немедленно, необходимо использовать EndThread из модуля SYSTEM, а 
не ExitThread из Win32^PI:	""————"* .. . , . . 

    procedure  EndThread(ExitCode   :   Integer); 

    Параметр ExitCode —это значение кода выхода для подзадачи. 
    В случае, если вы создаете подзадачу с помощью вызова BeginThread, Win32 
создает ядро подзадачи, связывает с ним вашу функцию подзадачи и передает вам 
обратно дескриптор. Если ваша подзадача заканчивает работу (при полном 
выполнении до конечного оператора end; или при вызове EndThread), Win32 
отмечает код выхода и ставит флаг завершения в состоянии ядра подзадачи. Это 
не разрушает самого ядра подзадачи — фактически ядро остается в своем 
собственном состоянии, ему просто сообщается, что подзадача завершена (ядро 
также передает сигнал об этом, и вы можете ожидать этого сигнала в 
подпрограммах Win32 WaitForSignalObject и WaitForMultipleObject. См. раздел 

    Глава 24. Работа с подзадачами	563 

    "Механизмы синхронизации" далее в этой главе). Вы немногое можете сделать 
с подзадачей в состоянии завершения. разве что запросить код выхода. К 
примеру, вы не можете ее перезапустить. Окончательно же ядро подзадачи 
разрушается только тогда, когда вы вызываете в вашей программе подпрограмму 
Win32 CloseHandle, передавая ей дескриптор подзадачи:	' 

    function CloseHandle   (hObject   :   THandle)    :   BOOL; 

    Параметр hObject является дескриптором подзадачи. Результатом выполнения 
функции будет True, если вызов прошел успешно и объект подзадачи разрушен, или 
False — в противном случае. 
    Избегайте напрасных расходов памяти и ресурсов в вашей программе. Вы 
должны вызвать CloseHandle. если закончили работу с подзадачей. 

    I Совет 1 

    ПРЕДОСТЕРЕЖЕНИЕ 
    Если вы вызвали CloseHandle, используя указатель на выполняющуюся 
подзадачу, то ее выполнение будет немедленно прервано. При этом все ресурсы, 
отданные подзадаче, не освобождаются. Подзадача останавливает работу и 
разрушается на том места, которого она достигла на момент вызова CloseHandle. 

    При создании подзадачи Win32 выделяет новый стек подзадачи из пространства 
адресов процесса (подзадачи не делят стек, каждая из них имеет свой 
собственный). Естественным результатом является то, что локальные переменные, 
вызываемые внутри подпрограмм, защищены: единственная подзадача, которая может 
их видеть, — та, которая использует при работе эту подпрограмму. Другие 
подзадачи, использующие в работе эту же подпрограмму, имеют другие локальные 
переменные. Размер стека подзадачи составляет 1 Мбайт, причем 16 Кбайт 
являются фиксированными. Этот размер соответствует размеру, принятому в Delphi 
по умолчанию, но вы можете изменить его, выбрав из меню Print^Options и указав 
в диалоговом окне Linker значения MinStackSize (минимальный размер стека) и 
MaxStackSize (максимальный размер стека). Эти две величины влияют на все 
подзадачи, созданные вашим приложением. Вы можете, если хотите, установить 
различные размеры стека для различных подзадач. (Но это не совсем хорошо, к 
тому же в любом случа е стек сразу не фиксируется.) 

    Приоритеты подзадач 

    Когда пользователь загружает приложение и Win32 создает процесс и 
первичную подзадачу, этому процессу присваивается класс приоритета, величина 
которого определяет, насколько важен этот процесс для пользователя и для 
системы. Из этого класса приоритета система извлекает значение приоритета, 
чтобы закрепить его за подзадачами процесса при их создании. Система будет 
расставлять подзадачи во всем процессе в соответствии с приоритетом, и в том 
случае, когда нет доступных для выполнения подзадач с высоким приоритетом, 
выполняются подзадачи с приоритетом более низкого уровня. Если несколько 
подзадач имеет один и тот же уровень приоритета, то они расставляются системой 
и выполняются одна за другой по кругу. 
    Приоритет, вычисленный из класса приоритетов процесса, известен как 
базовый приоритет (base priority), так как всем подзадачам, созданным для 
данного процесса, присваиваются приоритеты по отношению к базовому приоритету 
процесса. Другими словами, чтобы установить приоритет подзадачи, вы не должны 
запрашивать приоритет первичной подзадачи, а потом вычислять приоритет, 
который вы хотите назначить вашей подзадаче. Вы просто сообщаете системе о 
том, что хотите, чтобы подзадача имела такой же, более высокий или низкий 
приоритет по сравнению с базовым, и система справляется с этим сама. 
    Когда вы явно создаете процесс (а не просто запускаете программу с 
рабочего стола), вы декларируете для нового процесса класс приоритета. Можно 
использовать четыре класса приоритета, перечисленных в порядке возрастания 
важности в табл. 24.1: пассивный (idle), нормальный (normal), высокий (high) 
класс и класс реального времени (realtime). 

    Таблица 24.1 Основные приоритеты для классов приоритетов 

    Класс приоритета	Основной приоритет 

    IDLE_PRIORITY_CLASS	4 
    NORMAL_PRIORITY_CLASS__________7 (фоновый) или 9 (переднего плана)  ____   
____ ___    __ 
    HIGH_PRIORITY_CLASS	13 REALTIME_PRIORITY_CLASS                         24 


    564 Часть VI. Сложный материал 

    Чтобы установить класс приоритета какого-либо процесса, можете 
использовать подпрограмму Win32 SetPriori-tyClass. Программы, которые вы 
запускаете с рабочего стола, работают с нормальным классом приоритета. 
    Нормальный класс приоритета в действительности определяет два приоритета — 
нормальный класс переднего плана и нормальный фоновый класс. Если окно 
процесса является активным, то приоритеты его подза-*     дач автоматически 
повышаются относительно процессов, которые осуществляются в фоновом режиме. 

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

    function  SetThreadPriority:    (hThread:   THandle; 
    nPriority   :   Integer)    :   BOOL; 

    В этой функции hThread — дескриптор подзадачи, приоритет которой вы хотите 
изменить. nPriority — величина, которая описывает изменения относительно 
базового приоритета процесса. Это может быть одна из семи величин, которые 
определяются в модуле Delphi WINDOWS . PAS. Они представлены в табл. 24.2. 

    I Таблица 24.2 Относительные константы приоритета для SetThreadPriority 

    Константа Win32		Назначаемый приоритет 

    THREAD   PRIORITY	ABOVE  NORMAL	На 1 выше базового приоритета 
    THREAD_PRIORITY_	BELOW   NORMAL	На 1 ниже базового приоритета 
    THREAD   PRIORITY	HIGHEST	На 2 выше базового приоритета 
    THREAD   PRIORITY	IDLE	Для процесса с классом приоритета real  time — 
приоритет 16, для всех остальных классов — приоритет 1 
    THREAD_PRIORITY_	_LOWEST	На 2 ниже базового приоритета 
    THREAD_PRIORITY_	NORMAL	Базовый приоритет 
    THREAD_PRIORITY_	_TIME_CRITICAL	Для процесса с классом приоритета real  
time — приоритет 31, для всех остальных классов приоритетов — приоритет 1 5 

    Для того чтобы получить относительный приоритет подзадачи, вызовите, кроме 
SetThreadPriority, функцию GetThreadPriority: 

    functional   GetThreadPriority   (hThread   :   THandle)    :   Integer; 

    hTHread — дескриптор подзадачи, а результатом выполнения функции является 
одно из значений, используемых SetThreadPriority и описанных в табл. 24.2. 
    Прежде чем завершить захватывающую тему установки приоритетов, следует 
добавить парочку замечаний. Первое и самое важное — не делайте необдуманных 
поступков и не доводите все подзадачи до THREAD_PRIORITY_TIME_CRITICAL, чтобы 
сделать их выполнение предпочтительнее всего остального в системе. Если вы так 
поступите, то в зависимости от того, чем заняты ваши подзадачи, система в 
целом уподобится медленно ползущей улитке. Я знаю это по себе и сам сыплю 
проклятия, когда в Windows 95 вламывается упаковщик виртуальной памяти; и если 
вы пишете приложение с максимально высокими приоритетами подзадач, то будете 
ощущать его действие все время. 
    Второе замечание — вы должны сохранять высокие приоритеты только для тех 
задач, которые в этом нуждаются. Чаще всего написание программ для Windows — 
это вопрос кооперации. Если другие программисты не поднимают приоритеты своих 
подзадач в ущерб другим приложениям на машине, то и вы не должны. Повышайте 
приоритеты подзадач, где это необходимо (обыч 
    но это определяется критическими по времени операциями ввода-вывода), но 
не переборщите. Так же, как и во_в|земена Windows 3.x, когда вы как 
профессионал не написали ,6j>i сложного цикла без обработки сообщений, 
позволяющего выполняться другим программам под Windows, так и сегодня вы не 
напишете кода с искусственно завышенными приоритетами подзадач. 

    Глава 24. Работа с подзадачами 565 


    ПРЕДОСТЕРЕЖЕНИЕ 
    Что бы вы ни делали, не пишите коды, рассчитывая на то, что Win32 будет 
выполнять ваши равноприори-тетные подзадачи в определенном порядке. Не 
рассчитывайте на то, что первой будет выполнена подзадача 1 только потому, что 
вы создали подзадачу 1 раньше подзадачи 2. Может быть, так и будет, а может и 
нет. Всегда имейте в виду, что подзадачи выполняются в одно и то же время, как 
если бы ваша машина была несметное число раз распараллелена множеством 
процессоров Pentium. Если вы не сталкивались с этим, я разъясню вам, в чем тут 
суть. Если вы имеете, к примеру, подзадачу,^ожидающую события, а вы .сообщаете 
о нем из другой подзадачи, не рассчитывайте на то, что первая подзадачаГ будет 
выполниться немедленно и тем самым вытеснять подзадачу, которая находится в 
работе. Вы будете иногда правы, иногда — нет. Ву,„будете~правы в случае 
Windows 95, но ошибетесь в Windows NT. Пожалуйста, не доверяйте ничему! Ошибки 
подзадач отслеживать довольно сложно. 

    Защищенность подзадач 

    Итак, мы знаем, что подзадачи всегда делят одно и то же пространство 
памяти: nrjOjCj^aHCTBQ.jiaMS'Hjkix собственного пгхщесса. Все они имеют доступ 
к одним и тем же глобальным переменным. Локальные переменные защищены (они 
на-" ходятся в стеке), а каждая подзадача имеет отдельный стек. Но вот 
глобальные переменные... 
    Классическим примером того, что происходит с глобальными переменными и 
многими подзадачами, является связанный список. Представьте себе глобальный 
связанный список с несколькими подзадачами, одновременно добавляющими и 
удаляющими записи. Нетрудно понять, что получится в результате... 
    Обеспечить безопасность подзадач — значит сделать весь доступ к глобальным 
переменным защищенным. Код подзадачи должен "запирать" доступ к глобальным 
данным, которые подзадача хочет читать или обновлять, осуществлять доступ к 
ним. а затем вновь "отпирать" глобальные данные. Вообще говоря, подзадачи 
можно представить себе как участников эстафеты. Они должны схватить эстафетную 
палочку, прежде чем побежать по этапу, и, передав эстафету другой подзадаче, 
остановиться. Windows 95 и Windows NT используют слегка отличающиеся 
"эстафетные палочки" (в действительности они известны как объекты 
синхронизации), которые будут рассмотрены в следующих разделах. 
    Теперь представьте, что вы имеете такой блокирующий механизм вашего 
глобального связанного списка. Первая подзадача устанавливает блокировку, 
затем приступает к добавлению или уничтожению записи. После того как эта 
операция завершится и связанный список вновь будет приведен к целостному виду, 
подзадача снимет блокировку. Все другие подзадачи, которые ждали этого, теперь 
могут попытаться перехватить блокировку, но удастся это только одной из них. 
Эта победившая подзадача может по-своему обновить связанный список и снять 
блокировку. Вновь сумасшедшая драка за нее — и весь процесс повторяется 
(замечу, что я всего лишь использовал антропоморфические представления для 
подзадач, но в действительности сама система решает, какая подзадача 
"выиграет"; здесь нет правила, которое вы могли бы применить). 
    Важно помнить, что VCL не является защщценнои длягподзадач. Если вы хотите 
из подзадачи обновить окно (такое, как список или кнопка) в то время как 
другие подзадачи занимаются тем же самым, вы немедленно получите массу 
неприятностей. Потому;то корпорация Borland разработала и написала класс 
TThread (который мы рассмотрим ниже). Он имеет специальный механизм для того, 
чтобы дать возможность первичной подзадаче обновлять дисплей в интересах всех 
других подзадач, а не разрешать доступ каждой подзадачи внутрь VCL. 
    С другой стороны, менеджер кучи является защищенным для подзадач. 
Множество подзадач может одновременно размещаться в памяти и 
освобождать"пам'ять, используя GetMem и FreeMem и создавая и разрушая 
экземпляры классов. Менеджер кучи хранит все требования отсортированными и не 
позволяет их структурам превращаться в свалку мусора. 
    Система Borland Database Еп?те_тож?защищена для подзадач. Вы можете иметь 
много подзадач, получающих доступ к таблицам одновременно, без сбоев BDE. Для 
этого следует открыть индивидуальную сессию подзадачи, однако в этом мы не 
будем разбираться.

    Критические разделы 

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

    566	Часть VI. Сложный материал 

    туп к этой же структуре. Предположим также, что в это обновление входит 
много изменений, например добавление записи в связанный список. Если была 
другая подзадача, которая считывала структуру во время ее обновления, могут 
произойти крупные неприятности, так как обновятся не все связи. Для того чтобы 
обеспечить последовательный доступ к этой структуре (если это произнести 
развернуто, получится "гарантировать, что подзадачи будут иметь доступ к 
структуре поочередно"), во все подзадачи следует включить одни и те же типы 
кодов, которые выполняют следующее. 
    1.	Блокируют объект критического раздела. 
    2.	Считывают, обновляют или используют глобальную структуру. 
    3.	Снимают блокировку объекта критического раздела. 
    Предположим, что вы имеете подзадачи А и Б, каждая из которых выполняет 
этот код. Подзадача А блокирует объект критического раздела (Win32 SDK 
называет это входом в критический раздел). Подзадача Б вытесняет подзадачу А, 
начинает работать и пытается заблокировать критический раздел. Операционная 
система погружает в сон подзадачу Б, так как критический раздел уже 
заблокирован, а подзадача А вновь получает управление. Она обновляет данные, а 
затем снимает блокировку критического раздела (Win32 SDK называет это выходом 
из критического раздела). Операционная система замечает, что подзадача Б ждет, 
пока критический раздел станет разблокированным, и передает управление этой 
подзадаче, так что она может заблокировать критический раздел и выполнить свое 
собственное обновление данных. 
    Критический раздел является весьма эффективным. Ввести его можно очень 
быстро, если нет других подзадач, пытающихся сделать то же самое. По сути, это 
эквивалентно установке пары переменных в структуре данных критического 
раздела. Кроме того, критические разделы легко использовать. Но здесь есть 
несколько препятствий. Они могут использоваться только внутри_одного процесса 
(подзадачи в различных процессах не могут иметь доступа к критическому разделу 
в другом процессе^нГнётЪременного^jiepepbiва, связанного с критическими 
разделами. Последнее препятствие означает, что, если одна подзадачгГожидает 
введения критического раздела, а другая, которая уже взялась за критический 
раздел, еще не покинула его, работа первой подзадачи блокируется постоянно 
(или по крайней мере до тех пор, пока приложение не завершится). Объект 
синхронизации Win32 — мьютекс — устраняет оба вышеописанных недостатка. 
Заметим, однако, что мьютексы имеют свои собственные недостатки. 
ПринцигТйальным'является то, что испол ьзование мьютексов требует 
бо_льше__времени, чем использование критического раздела. Таким образом, 
используйте мьютексы только "в том случае,_если нужен механизм блокировки, 
затрагивающий несколько процессов, или если необходим этот временной 
перерыв.	" ' •••--... 
    Для нормально отлаженной программы критические разделы чрезвычайно 
полезны, так что для упрощения их использования стрит инкапсулировать 
критические разделы функционально в класс. В листинге 24.1 показана такая 
инкапсуляция'™'"названная 'TsePadlock. Вы можете" найти исходный текст 
программы, PADLOCK. PAS (как и все другие листинги, каталоги которых приведены 
в их описаниях), на дополнительно распространяемой дискете. 

    Листинг 24.1. \UDELPHI3\CHP24\PADLOCK. PAS. Класс Padlock, инкапсуляция 
критических разделов 

    unit Padlock; 
    interface 
    uses 
    Windows; 
    type 
    TsePadlock = class private 
    FCritSect : TRTLCriticalSection; protected 
    procedure SetLocked(L : boolean); public 
    constructor Create; destructor Destroy; override; property Locked : 
boolean write SetLocked; end; 
    implementation 
    constructor TsePadlock.Create; begin 
    InitializeCriticalSection(FCritSect); end; 


    Глава 24. Работа с подзадачами	567 


    destructor TsePadlock.Destroy; begin 
    DeleteCriticalSection(FCritSect); end; 
    procedure TsePadlock.SetLocked(L : boolean); begin 
    if L then 
    EnterCriticalSection(FCritSect) else 
    LeaveCriticalSection(FCritSect) ; end; 
    end. 

    Конструктор Create инициализирует внутренний критический раздел, 
деструктор Destroy уничтожает его. Для то го чтобы ввести критический раздел, 
установите свойство Locked равным True, чтобы выйти из него, установите свой 
ство Locked равным False. Вот и все. 
    Для того чтобы проверить инкапсуляцию критического раздела, в листинге 
24.2 организован последовательный доступ к окну консоли двух независимых 
подзадач, причем обе одновременно пытаются что-то там написать. Одна подзадача 
пишет ряды строк на английском, а вторая — на французски. Если вы не 
установили значения характеристики компилятора UsePadlock , то увидите, что 
подзадачи накладываются одна на другую, в результате чего на консоли будет 
записа-на"сплошная чепуха. 

    | Листинг24.2. \UDELPHI3\CHP24\TESTPADLOCK.PAS. Испытываем класс Padlock 
program TestPadlock; 

    {$APPTYPE CONSOLE) 
    // Удалите это определение для компилятора (поставив точку перед // знаком 
$), чтобы посмотреть, что происходит, когда Padlock // не используется при 
выводе на экран. {$DEFINE UsePadlock} 
    uses 
    SysUtilp, 
    Windows, 
    Padlock; 
    var 
    ConsolePadlock : TsePadlock; 
    procedure SafeWriteLn(const S : string); begin 
    ($IFDEF UsePadlock} 
    ConsolePadlock.Locked := True; 
    try 
    writeln (S) ; 
    finally 
    ConsolePadlock.Locked := False; 
    end; 
    {$ELSE} 
    writeln(S); 
    {$ENDIF} end; 
    function EnglishThreadRoutine(P : pointer) : integer; var 
    ThreadID : integer; 
    i       : integer; begin 
    Result := 0; 


    568	Часть VI. Сложный материал 


    ThreadID := GetCurrentThreadID; for i := 1 to 20 do 
    SafeWriteLn(Format('[%x] current line: %d', [ThreadID, intend; 
    function FrenchTreadRoutine(P : pointer) : integer; var 
    ThreadID : integer; 
    i        : integer; begin 
    Result := 0; 
    ThreadID := GetCurrentThreadID; 
    for i := 1 to 20 do 
    SafeWriteLn(Format('[%x] ligne courant: %d', [ThreadID, i])); end; 
    var 
    Handles : array [0..1] of integer; TreadID : integer; 
    begin 
    FillChar_(_Handles, sizeof(Handles) , 0) ; Cons'oTePadlock := TsePadlock. 
Create; try 
    Handles[0] := BeginThread(nil, 0, EnglishThreadRoutine, 
    nil, 0, ThreadID); Handles[1] := BeginThread(nil, 0, FrenchThreadRoutine, 
    nil, 0, ThreadID); 
    WaitForMultipleObjects(2,    @Handles,   True,    INFINITE); finally 
    if   (Handles[0]   <>   0)   then CloseHandle(Handles [0]); if   
(Handles[1]   <>   0)   then CloseHandle(Handles[1]); ConsolePadlock.Free; end; 
    writeln ('Press   <Enter>   to  close...'); readln; end. 

    Сначала создается консоль Padlock, а затем — две подзадачи, которые будут 
писать в окне консоли: первая выполняет функцию EnglishThreadRoutine. чтобы 
писать в окне фразы на английском языке, а вторая— функцию FrenchThreadRoutine 
для французских фраз. После этого первичная подзадача ожидает завершения обеих 
подзадач, прежде чем заняться очисткой памяти. В каждой функции подзадачи для 
того, чтобы написать символьную строку на консоли, используется подпрограмма 
SafeWriteLn. Она работает, получая консоль Padlock, записывая символьную 
строку и освобождая консоль Padlock.	•-•....,.—..,..--••
    Для использования Padlock вы создаете ее в некотором месте вашего 
приложения, обычно в начале (очевидно, что перед тем местом, где создается 
подзадача, которая будет использовать Padlock). Разумеется, вы разрушаете 
Padlock в конце программы, после всех подзадач, кроме первичной. В лучшем 
случае вы создаете Padlock для каждой глобальной структуры данных, к которым 
имеется доступ в вашем приложении. Предположим, что вы имеете две структуры 
данных, которые применяются несколькими подзадачами. Создайте две консоли 
Padlock, по одной для каждой структуры. Когда вы захотите получить доступ к 
первой структуре, заблокируйте первую консоль Padlock, осуществите доступ к 
структуре и, сделав все необходимое, разблокируйте эту консоль. Повторите то 
же самое и для второй структуры. 
    Замечу здесь, что вы должны быть очень осторожны с взаимными блокировками, 
когда используете несколько консолей Padlock (или критические разделы). 
Предположим, имеется две консоли Padlock и две подзадачи. Чтобы можно было 
выполнить некоторые специфические манипуляции данными, эти подзадачи должны 
заблокировать обе консоли Padlock. Если подзадача А блокирует Padlock2 после 
Padlockl, тогда как подзадача Б блокирует Padlockl после Padlock2, то вполне 
возможно, что подзадача А, которая блокирует Padlockl, будет вытеснена в 
пользу подзадачи Б, которая блокирует Padlock2 позже. И теперь ни подзадача А, 
ни подзадача Б не могут работать, так как следующая консоль Padlock, 
блокировки которой они ожидают, уже заблокирована одной из них. Таким образом, 
возникает ситуация клинча (известный боксерский термин, как нельзя более 
подходящий к нашей ситуации): заблокированными оказы

    Глава 24. Работа с подзадачами	569 

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

    Разнообразные функции 

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

    function  GetCurrentThread   :   Thandle; 

    Эта функция возвращает псевдодескриптор текущей работающей подзадачи, 
который называется псевдодескриптором из-за того, что это просто справочный 
дескриптор подзадачи. Вы не должны закрывать его после того, как выполните 
подзадачу (фактически CloseHandle игнорирует требование закрыть 
псевдодескриптор). Псевдодескриптор может использоваться толькотой подзадачей, 
которая вызывает GetCurrentThread, но не подзадачами, которые на него 
ссылаются. Вы можете создать "настоящий" дескриптор подзадачи, вызвав функцию 
Win32 DuplicateHandle (такой дублированный дескриптор может!5ыть закрыт без 
всяких затруднений). Лично я считаю, что функция GetCurrentThread немного 
свихнувшаяся, и предпочитаю сохранять возвращаемую ссылку тогда, когда создаю 
подзадачу, а не использовать эту сумасшедшую функцию.	" .. "" -————— 

    function  GetCurrentThreadID   :   DWORD; Эта функция возвращает 
уникальный ID (идентификатор) текущей подзадачи. 
    function TerminateThread(hThread:   THandle; 
    dwExitCode:   DWORD)    :   BOOL; 

    Она завершает выполнение подзадачи, заданной дескриптором hThread, и 
устанавливает код выхода dwExitCode По тем же многим причинам, по которым вы 
не должны вызывать базовые CreateThread и ExitThread,.не.следует вызывать и 
TerminateThread. Она, к примеру, не освобождает ресурсы. Гораздо лучше 
установить флаг, который подзадача постоянно опрашивает (или сигнал события в 
Win32), и заставить саму программу подзадачи убраться за собой и завершиться. 
Используйте TerminateThread только в случаях крайней необходимости. 

    function  SuspendThread   (hThread   :   THandle)    :   DWORD; 

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

    function ResumeThread   (hThread   :   THandle)    :   DWORD; 

    Данная функция возобновляет работу приостановленной подзадачи. Подзадача 
возобновляется с того места, на котором ее приостановил вызов SuspendThread. 
Замечу, что ResumeThread не влияет на подзадачи, которые ожидают сигнала от 
ядра (они описываются в следующих разделах), или на те подзадачи, для которых 
стал доступным критический раздел. 

    Механизмы синхронизации 

    Синхронизация применительно к подзадачам Win32 означает погружение 
подзадач в сон (тем самым осуществляется разделение их процессорного времени) 
вплоть до некоторого события. К примеру, рабочая подзадача "засыпает" до тех 
пор, пока подзадача файлового ввода-вывода не завершит чтение из файла в буфер 
для исходной подзадачи процесса. Вы можете сделать это разделение времени, 
например, опрашивая некоторые глобальные переменные (подзадача файлового 
ввода-вывода по окончании работы так устанавливает значение True для 
глобальной логической переменной, чтобы рабочая подзадача знала, когда буфер 
заполнен; однако при этом рабочая подзадача все время кругится^в цикле, как 
белка в колесе, считывая значение переменной). Но это плохо воспринимается 
Win32, так как слишком ужнеэффективно. Более того, если рабочая подзадача 
имеет высокий приоритет, то из-за своих бессмысленных циклов она 
заблокируетТтСдзадач> файлового ввода-вывода: никакие данные не смогут быть 
прочитаны и, следовательно, рабочая пр ограмма никогда ничего не сможет 
сделать. Приложение усердно отработает до зависания. 
    Скажем хором: проверка глобальных переменных— плохое дело] На наше 
счастье, есть два способа погрузить подзадачу в сон до выполнения какого-либо 
события — вызов подпрограмм Win32 WaitForSignalObject и WaitForMul-tipleObject. 

    570	Часть VI. Сложный материал 

    Некоторые объекты ядра (KERNEL) могут отсылать сигналы. (О, еще один 
термин!) Единственный способ дать наглядное представление о том, что означает 
отослать сигнал, — сравнить этот объект с сушилкой для одежды. После того как 
сушилка управилась с очередной партией белья, зуммер начинает пищать, (а) 
сводя вас с ума (дом обычно в этот момент пустой и тихий, а вы читаете роман 
ужасов Ден Симона) и (б) сообщая, что пора вынимать одежду из сушилки. Зуммер 
передает сигнал о том, что сушилка закончила работу (она переведена в 
сигнализирующее состояние), до тех пор, пока его не отключат (сушилка 
переходит в несигнализирующее состояние). 
    Объекты приводятся в состояние сигнализирующих различными способами, и эти 
способы определяются природой объекта. В этой главе уже говорилось, что по 
окончании выполнения подзадачи соответствующий объект ядра сигнализирует об 
этом. Вы можете ожидать этого сигнала, чтобы узнать, когда подзадача будет 
завершена. Вы можете ожидать его, используя подпрограммы Win32 
WaitForSingleObject и WaitForMultipleObject. Короче говоря, первая из 
вышеназванных подпрограмм позволяет погружать подзадачу в сон до тех пор, пока 
заданный объект ядра не станет отсылать сигналы. Вторая предоставляет 
возможность усыплять подзадачу до тех пор, пока хотя бы один из нескольких 
объектов не станет сигналить либо пока они все не сделают этого. 

    function WaitForSingleObject    (hHandle:   THandle; 
    dwMilliseconds:    DWORD)    :    DWORD; function  WaitForMultipleObject   
 (nCount   :   DWORD; 
    IpHandles   :   PWOHandleArray; 
    bWaitAll:   BOOL; 
    dwMilliseconds:   DWORD)    :   DWORD; 

    В функции WaitForSingleObject параметр hHandle— это дескриптор объекта 
ядра, сигнала от которого вы ожидаете; dwMilliseconds определяет, как долго вы 
хотите ждать (вы можете передать здесь константу INFINITE, чтобы заставить 
этот вызов ожидать сигнала до бесконечности). Возвращаемой величиной является константа 
WAIT_FAILED или WAIT_OBJECT_0. 
    u	Существует такая ситуация с мьютексами, когда результатом вызова 
WaitForSingleObject может быть WAIT_ABANDONED. Это значение возвращается в том 
случае, если подзадача, предварительно владевшая мьютексом, закончила работу 
до освобождения мьютекса. Мьютекс описывается как покинутый. 
    В функции WaitForMultipleObj ect параметр nCount — это число дескрипторов 
объектов ядра, которые вы хотите обслуживать, a IpHandles — указатель на 
массив этих дескрипторов. bWaitAll равно True, если вы ожидаете сигналов от 
всех объектов, или False, если вы хотите закончить вызов после того, как хотя 
бы один объект подаст сигнал. dwMilliseconds определяется, как и для 
WaitForSingleObject. Возвращаемое вновь значение— или WAIT_FAILED, или 
указание, какой объект подал сигнал. Если в исходном вызове bWaitAll было True 
и ожидание не было прервано, значит, все объекты подали сигналы, и вы не 
можете сделать никакого вывода из возвращаемого значения (если только оно не 
было WAIT_FAILED). Если же, наоборот, в исходном вызове bWaitAll было False и 
ожидание не было прервано, то возвращаемое значение показывает индекс 
дескриптора объекта, который подал сигнал. Для_расчета индекса надодще^ть 
возвращаемое значение из WAIT OBJECT 0. В том случае, если два или более 
объектов передали сигналы одновременно, возвращаемое значение указывает на 
дескриптор с меньшим индексом. 
    Процессы, подзадачи, файлы, ввод с консоли, сообщения об изменениях файла, 
мьютексы, семафоры и события — вот те объекты ядра,_которые могут подавать 
.сигналы. Процессы и подзадачи издают звуки по окончании работы. В этой 
гла-~ве не затрагиваются файлы, ввод с консоли и сообщения об изменениях 
файла. Мьютексы, семафоры и события описываются в следующих разделах настоящей главы. 
    Для того чтобы рассмотреть пример ожидания сообщения от объекта, вернемся 
к тестовой программе Padlock, которую мы рассматривали, когда речь шла о 
критических разделах. Тогда я опустил описание фрагмента основной, 
подпрограммы, в котором вызывается WaitForMultipleObject. Вернувшись к нему, 
вы увидите, что основная подпрограмма ожидает окончания выполнения обеих 
подзадач и подачи сигналов. 

    Праздник смертельного объятия 

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

    Глава 24. Работа с подзадачами	571 

    Вот мы и подошли к проблеме взаимной блокировки: вы должны быть очень 
внимательны, когда используете в приложении объекты синхронизации. 
Постарайтесь всегда использовать их в одном и том же порядке. 
    Если вы оказались в ситуации взаимной блокировки, значит, подзадачи больше 
не выполняют никакой работы. Необходимо отладить код подзадачи, обращая особое 
внимание на места, в которых вы вызываете метод WaitForSir,-gleObject или 
WaitForMultipleObject. Возможно, наилучшим способом отладки кода подзадачи 
является ее запуск с рабочего стола с выводом отладочной информации (печально 
то, что код подзадачи отрабатывается без замечаний, пока вы пользуетесь 
отладчиком). Представим, что вы запустили на выполнение отлаженную подзадачу и 
отслеживаете код до тех пор, пока не получите метода Wait, и предположим, что 
подзадача будет его ожидать. Перейдем к следующей подзадаче и сделаем то же 
самое. Одни подзадачи будут снимать блокировку других. Совершенно случайно вы 
обнаружите, что отлаженная подзадача окончательно заблокирована из-за того, 
что заблокирована и другая подзадача. На этой стадии вы можете додуматься, как 
исправить ситуацию. Попытайтесь использовать рассуждения, которые приводились 
в этой главе при рассмотрении проблемы, связанной с кодами подзадач. 

    События 

    События — это простые механизмы синхронизации. Они дают одной подзадаче 
возможность информировать другую. или другие, о том, что произошло какое-то 
событие или была завершена некоторая работа. Обращаю ваше внимание, что 
события Win32 не должны ничего делать с событиями Delphi: первые являются 
объектами синхронизации, а вторые — методами, которые обычно запускаются 
сообщениями окна при получении этих сообщений формой окна. 
    Событие имеет два состояния— сигнализирующее и несигнализирующее. 
Подзадача вызывает WaitForSingleOb-ject (или альтернативный ему 
WaitForMultipleObject) с дескриптором события. Если событие находится в 
несигнализирующем состоянии, то одна подзадача ждет до тех пор, пока другая 
подзадача не заставит событие перейти в сигнализирующее состояние (это 
известно как установка события) и в это время система возобновляет выполнение 
первой подзадачи. 
    Существует два типа событий — события с автоматической переустановкой и 
события, переустанавливаемые вручную. При автоматической переустановке, после 
того как событие передало сигнал и возобновилось выполнение подзадачи, 
ожидающей этого сигнала, событие переустанавдивается автоматически 
(устанавливается в несигнализирующее состояние). При переустановке вручную 
событие должно переустанавливаться в несигнализлрующее состояние вызовом 
соответствующей подпрограммы Win32. 
    Основное различие проявляется, когда вы имеете несколько подзадач, 
ожидающих одного и того же события. В случае автоматической переустановки 
только одна подзадача прекращает ожидание, если вы установили это событие 
(после того как одна подзадача освободилась, событие переустанавливается и 
другие подзадачи остаются в состоянии ожидания). При переустановке события 
вручную освобождаются все подзадачи, ожидавшие этого события. 
    Вы можете выполнить еще о.д,ну операцию с событием (еще одну потому, что 
другие — это установка и переустановка события): работать с ним в пульсирующем 
режиме. Это полезно только в случае переустановки событий вручную. Работая в 
пульсирующем режиме с событием, переустанавливаемом вручную, вы делаете 
следующее: устанавливаете событие, освобождаете все подзадачи, ожидающие этого 
сигнала, а затем переустанавливаете событие (и все за один вызов). Для 
автоматической переустановки пульсация события эквивалентна установке события: 
освобождается максимум одна подзадача, а затем переустанавливается событие. 
    В листинге 24.3 событие Win32 инкапсулируется классом TseWin32Event 
(исходный текст находится в файле WIN32EVENT. PAS на дискете). 

    Листинг 24.3. \UDELPHi3\CHP24\wiN32EVENT. PAS. Класс Win32Event - 
инкапсуляция событий Win32	I 

    unit Win32Event; 
    interface 
    uses 
    SysUtils, Windows; 
    type 
    EWin32EventError = class(Exception) private 
    FErrorCode : intege-r; public 
    constructor Create(aErrorCode : integer); property ErrorCode : integer 
read FErrorCode; end; 


    572	Часть VI. Сложный материал 


    TEventResetType = (erAuto, erManual); 
    TseWin32Event = class     private      
    FEvent    : THandle; protected 
    procedure SetReady(R : boolean); public 
    constructor Create(const aEventName : string; 
    aResetType : TEventResetType; alnitiallySignaled : boolean); destructor 
Destroy; override/procedure JPu.Lg.g.; 
    function WaitForEvent(TimeOut : integer) : boolean; property Handle : 
THandle read FEvent; property Ready : boolean write SetReady; end; 
    implementation 
    constructor EWin32EventError.Create(aErrorCode : integer); begin 
    FErrorCode := aErrorCode; 
    inherited Create(Format('Win32 event error: %d', [aErrorCode])); end; 
    constructor TseWin32Event.Create(const aEventName : string; 
    aResetType : TEventResetType; alnitiallySignaled : boolean); begin 
    FEvent := CreateEvent(nil, (aResetType = erManual), 
    alnitiallySignaled, PChar(aEventName)); if (FEvent = 0) then 
    raise EWin32EventError.Create(GetLastError); end; 
    destructor TseWin32Event.Destroy; begin 
    if (FEvent <> 0) then CloseHandle(FEvent); end; 
    procedure TseWin32Event.Pulse; begin	• 
    _PulseEvent (FEvent) ;  >v\  A<^i_ endl        ' ~	•       .  .   . 
    procedure TseWin32Event.SetReady(R : boolean); begin 
    if R then 
    SetEvent(FEvent) else " 
    ResetEvent(FEvent); end; 
    function TseWin32Event.WaitForEvent(TimeOut : integer) : boolean; begin 
    Result := WaitForSingleObject(FEvent, TimeOut) <> WAIT_FAILED; 
    end. 

    Конструктор Create создает дескриптор события. Вы определяете имя события, 
тип, который хотите создать (автоматическая переустановка или переустановка 
вручную), и будет ли событие изначально сигнализирующим. Деструктор Destroy 
закрывает дескриптор события. Метод WaitForEvent позволяет заставить подзадачу 
ждать сигнала от 

    Глава 24. Работа с подзадачами	573 

    этого события. Параметр TimeOut определяет, как долго вы хотите ждать 
(можете передать этому параметру INFINITE чтобы устранить перерыв в ожидании). 
Метод Pulse работает с событием в пульсирующем режиме. Свойство Ргор-ejrty 
можно только записывать: установка True вынуждает событие отсылать сигнал, 
установка False делает событие несигнализирующим. 
    Для облегчения жизни я декларировал новый особый класс, чтобы отслеживать 
любые ошибки, которые могут быть возвращены вызовом Win32 CreateEvent. Этот 
особый объект хранит код ошибки, который сообщается подпрограммой Win32 
GetLastError в том случае, если что-то сработало неверно в CreateEvent. 
    Название, которое вы передаете конструктору Create, используется для 
указания имени события. Это имя будет глобальным для системы, так что все 
подзадачи в системе, даже те, которые относятся к другим" процессам, видят это 
имя. Это может вызвать следующую проблему: если в момент вызова конструктора 
Create это имя уже существует, вы не можете создать новый объект события. 
Скорее всего, вы получите дескриптор уже существующего объекта события 
(открытый счет ссылок на объекты получит приращение, и, таким образом, 
дескриптор сможет закрываться дважды). Если это событие используется в другом 
процессе для совершенно других целей, синхронизация вашей подзадачи будет 
отличаться слишком буйной фантазией, чтобы ее укротить. Так что с большим 
вниманием относитесь к присвоению названий своим событиям. 
    Программа TestWin32Event на дискете показывает пример работы с классом событий. 
    Определив, наконец, класс событий Win32, займемся другой типичной 
многозадачной проблемой, которая чрезвычайно сложна и значительна. Обычно, 
когда мы хотим разделить структуру глобальных данных между двумя подзадачами, 
мы добиваемся, чтобы эти подзадачи были отделены одна от другой (как читатель 
и писатель). Этот прием был продемонстрирован при создании Padlock для 
структуры данных. Когда бы вы ни ожидали доступа к структуре данных, то ли для 
операции чтения, то ли для записи, вы блокируете Padlock, осуществляете 
доступ, а затем снимаете блокировку с Padlock. Если в основное время работы 
подзадачи будут просто считывать структуру данных, то это пустая трата 
ресурсов, так как все подзадачи могли бы получить доступ к данным 
одновременно. Предполагается, что структура или величины в этой структуре не 
будут изменяться вследствие чтения данных подзадачами. Замечу, что 
определенные структуры, такие как разветвленные деревья, переупорядочиваются 
самостоятельно, когда из структуры считывается запись данных (в нашем случае 
не будем принимать во внимание типы структур, использующие оптимизацию). 
Единственная проблема возникает, когда подзадача пытается обловить структуру 
данных. В этом случае структура должна быть заблокирована так, чтобы все 
данные могли быть обновлены в ходе единственной операции. Во время этой 
операции никакйе'дру-гие подзадачи_не могут иметь досту'паГк чтению "ИлИ"
Зап"иси структуры данных.	! 
    Таким образом, мы хотйм*йметь вид объекта синхронизации, который позволит 
многим читающим подзадачам осуществлять одновременный доступ к глобальной 
структуре данных, но будет разрешать только одной пишущей подзадаче обновлять 
данные за один заход, блокируя все остальные подзадачи (и читателей, и 
писателей) на время, пока она работает. Здесь также возникает "еще одна 
проблема; может быть так много активных читающих подзадач, что окажется, что 
пишущая подзадача постоянно блокирована в ожидании прекращения доступа на, 
чтение. Так что объект синхронизации должен устанавливать очередность читающих 
и пишущих подзадач. 
    JOiaccT's'eGetkeeper (в модуле GATEKEEPER. PAS) определяет такой объект 
синхронизации. А называется он так потому, что может направлять и 
организовывать тех, кто может иметь доступ к данным через "ворота": или толпа 
читателей, или только единственный поддерживаемый писатель. Я называю его 
привратником (gatekeeper), так как образно связываю его с привратниками в Kew 
Garden в Лондоне, впускающими туристов, если нет какого-нибудь ремонта 
(листинг 24.4). 

    Листинг 24.4. \UDELPHI3\CHP24\GATEKEEPER. PAS. Класс Gatekeeper, решение 
проблемы "много читателей/много писателей" 

    unit Gatekeeper; interface 
    uses 
    Windows, Padlock, Win32Event; 
    type 
    TseGatekeeper = class private 
    Padlock        *    TsePadlock; WaitingReaderCount   integer; 
RunningReaderCount   integer; ReaderCanStart      TseWin32Event; 
RunningWriterCount   integer; WaitingWriterCount   integer; 


    574	Часть VI. Сложный материал 


    WriterCanStart     : TseWin32Event; public 
    constructor Create(aGateName : string); destructor Destroy; override; 
procedure StartReading; procedure StartWriting; procedure StopReading; 
procedure StopWriting; end; 
    implementation 
    constructor TseGatekeeper.Create(aGateName : string); begin 
    Padlock := TsePadlock.Create; 
    ReaderCanStart := TseWin32Event.Create(aGateName+'Read', erAuto, false); 
    WriterCanStart := TseWin32Event.Create(aGateName+'Write', erAuto, false); end; 
    destructor TseGatekeeper.Destroy; begin 
    WriterCanStart.Free; 
    ReaderCanStart.Free; 
    Padlock.Free; end; 
    procedure TseGatekeeper.StartReading; begin 
    Padlock.Locked := true; 
    if (RunningWriterCount > 0) or (WaitingWriterCount > 0) then begin 
inc(WaitingReaderCount); Padlock.Locked := false; 
ReaderCanStart.WaitForEvent(INFINITE); end else begin 
    inc(RunningReaderCount); Padlock.Locked := false; end; end; 
    procedure TseGatekeeper.StartWriting; begin 
    Padlock.Locked := true; 
    if (RunningWriterCount > 0) or (RunningReaderCount > 0) then begin 
inc(WaitingWriterCount); Padlock.Locked := false; 
WriterCanStart.WaitForEvent(INFINITE); end else begin 
    inc(RunningWriterCount); Padlock. Locked := false/end; end; 
    procedure TseGatekeeper.StopReading; begin 
    Padlock.Locked := true; dec(RunningReaderCount); 
    if (RunningReaderCount = 0) and (WaitingWriterCount > 0) then begin 
dec(WaitingWriterCount); inc(RunningWriterCount); WriterCanStart.Ready := 
true; end; 
    Padlock.Locked := false; end; 


    Глава 24. Работа с подзадачами	575 


    procedure TseGatekeeper.StopWriting; begin 
    Padlock.Locked := true; dec(RunningWriterCount); if (WaitingReaderCount > 
0) then begin while (WaitingReaderCount > 0) do begin dec(WaitingReaderCount); 
inc(RunningReaderCount); ReaderCanStart.Ready := true; end; end else begin 
    dec(WaitingWriterCount); inc(RunningWriterCount); WriterCanStart.Ready := 
true; end; 
    Padlock.Locked := false; end; 
    end. 

    Класс TseGetkeeper определяет четыре метода: StartReading, StartWritting, 
StopReading и StopWriting. По сути, он использует Padlock и два объекта 
Win32Event для поддержки двух очередей и для последовательного доступа 
читателей и писателей. 
    Рассмотрим использование этого класса с точки зрения читающей подзадачи. 
Для того чтобы получить доступ к структуре данных, которую защищает класс 
Gatekeeper, сначала вызывается метод StartReading. Он проверяет, есть ли хоть 
какая-то пишущая программа, которая ожидает запуска или уже работает. Если 
таковая имеется, подзадача присоединяется к очереди ожидающих 
подзадач-читателей и ждет сигнала от события ReaderCanStart. Если подобной 
подзадачи-писателя нет, то метод допускает, чтобы читающая подзадача работала 
без помех. Завершив операцию чтения, такая подзадача вызывает метод 
StopReading. Он просматривает, есть ли еще работающие подзадачи-читатели. Если 
таковых нет, то он проверяет, есть л и ожидающие подзадачи-писатели. Обнаружив 
их, он передает сигнал событию WriteCanStart. Это является причиной того, что 
одна (и только одна) пишущая подзадача освобождается и начинает работать. 
    Теперь рассмотрим этот класс с точки зрения пишущей подзадачи. Чтобы 
получить доступ к готовой к обновлению структуре данных, подзадача вызывает 
метод StartWritting. Этот метод проверяет, есть ли работающие в данный момент 
подзадачи-читатели или писатели. Если таковые имеются, то подзадача 
добавляется к очереди пишущих подзадач в ожидании сигнала от события 
WriteCanStart. Если нет работающих в данный момент подзадач, подзадача 
продолжает выполняться. Завершив обновление данных, подзадача должна вызвать 
метод StopWriting. Сначала он просматривает, есть ли какие-нибудь читающие 
подзадачи, которые ожидают запуска. Если таковые есть, то он освобождает их 
всех, передавая событию ReaderCanStart сигналы— один для каждой ожидающей 
подзадачи-читателя. Если таких подзадач нет, метод проверяет, есть ли 
ожидающие подзадачи-писатели. Если какая-то из них присутствует, она 
освобождается по сигналу события WriteCanStart. 
    Внутренний объект Padlock используется для организации последовательного 
доступа к внутренним счетчикам для ожидающих и работающих подзадач чтения и записи. 
    Как вы можете видеть, класс Gatekeeper значительно упрощает выполнение 
сценария "много читателей/один писатель". Этот класс управляет читателями и 
писателями в их собственных очередях. (Обращаю ваше внимание, что эти очереди 
не являются очередями FIFO (первым вошел — первым вышел), так как мы не знаем, 
какую подзадачу система освободит первой: мы знаем только, что она освободит 
одну из них.) Алгоритм, введенный в этот класс, создан таким образом, чтобы 
сначала ветвь подзадач-читателей добралась до защищенных данных, затем к ним 
получила доступ единственная подзадача записи (в том случае, если она 
действительно одна), затем — вновь ветвь подзадач-читателей, затем — подзадача 
записи и т.д. 
    Есть один интересный анекдот, связанный с этим делом. Мораль его такова: 
только очень умный может сделать так. чтобы много подзадач, объединенных 
вместе, ничего не делали. Рассмотрим вновь метод StopWriting. Если есть 
ожидающие подзадачи-читатели, то он освобождает их всех, передавая событию 
ReaderCanStart сигналы, по одному на каждую ожидающую подзадачу, в простом 
цикле while. Все хорошо и замечательно, но я как-то подумал, что смогу сделать 
и получше. Сделал событие ReaderCanStart вручную переустанавливаемым и стал 
работать с ним в пульсирующем режиме. Все ожидающие подзадачи должны были бы 
освободиться одновременно. Звучит заманчиво? Подумайте немного об этом, прежде 
чем продолжить читать. 
    Мой измененный код выглядел примерно так. 

    if   (WaitingReaderCount   >   0)   then  begin 
    RunningReaderCount   :=  WaitingReaderCount; 
    WaitingReaderCount   :=   0; 
    ReaderCanStart.Pulse; end; с 


    576	Часть VI. Сложный материал 

    Звучит хорошо, выглядит великолепно! Рассмотрим метод StartReading. Если 
он устанавливает, что подзадача должна подождать, он увеличивает счет 
ожидающих подзадач-читателей, снимает блокировку с Padlock и ожидает события 
ReaderCanStart. Но этот метод может быть вытеснен после разблокирования 
Padlock и перед введением ожидания события. В таком случае счетчик ожидающих 
подзадач увеличится. Предположим, что подзадача, которая получила управление, 
отрабатывает мой "улучшенный"' код. Все подзадачи-читатели (за исключением 
нашей гипотетической подзадачи) будут освобождены от ожидания, но Gatekeeper 
теперь полагает, что работающих подзадач на одну больше, чем на самом деле. 
Если вы рассмотрите следующий фрагмент программы в этом классе, то увидите, 
что ожидающие подзадачи-писатели никогда, повторяю, никогда не получат 
управления в свои руки, даже если все читающие подзадачи будут завершены 
(привратник будет всегда думать, что есть еще хотя бы одна работающая 
подзадача-читатель). Когда наша ги потетическая подзадача-читатель вновь 
получит управление, она будет выполняться до тех пор, пока не понадобится 
событию ReaderCanStart. В этом случае она будет заблокирована навсегда, так 
как это событие только передает сигналы подзадаче-писателю, вызывая метод 
StopWriting, и я прямо-таки вижу, как блокируются все пишущие подзадачи. 
Плохие новости, особенно для такого простого "улучшения"... 
    Обращаю ваше внимание, что в правильном фрагменте программы не имеет 
значения, будет ли подзадача-читатель вытесняться перед началом ожидания, так 
как событие в любом случае начинает сигнализировать и остается сигнализирующим 
до тех пор, пока его не дожидается читающая подзадача. Эффект получился не 
тот, которого ожидали. 

    Мьютексы 

    Мьютексы (MUTual Exclusions — взаимоисключения) создавались подобно 
критическим разделам и функционируют в их же стиле. Есть, однако, три важных 
отличия. Первое состоит в том, что мьютексы являются глобальными для системы и 
могут быть видны из всех процессов. Второе отличие — вы можете ожидать их 
сигналов с перерывом (критические разделы не имеют: перерыва, когдаОжидают 
сигнала стать доступными для применения). Третье, возможно, самое главное, — 
использование мьютекса отнимает значительно больше времени, чем использование 
критического раздела. Принимая во внимание все эти отличия, можно легко 
определить, нужно ли использовать мьютекс или критический раздел. Если вам 
нужна скорость и вы привязываетесь только к одному процессу, воспользуйтесь 
критическим разделом. Если же вы хотите иметь последовательный доступ к данным 
из многих подзадач многих процессов, то придется применять мьютекс. 
    Я не буду здесь подробно описывать мьютексы, а просто введу мою обычную 
инкапсуляцию класса в листинге 24.5. 

    Листинг 24.5. \UDELPHI3\CHP24\MUTEX. PAS. Класс Mutex - инкапсуляция мьютексов 

    unit Mutex; 
    interface 
    uses 
    SysUtils, Windows; 
    type 
    EMutexError = class(Exception) private 
    FErrorCode : integer; public 
    constructor Create(aErrorCode : integer); property ErrorCode : integer 
read FErrorCode; end; 
    TseMutex = class 
    private 
    FHandle : THandle; 
    protected 
    public 
    constructor Create(aName : string); destructor Destroy; override/function 
Lock(aTimeOut : integer) : boolean; procedure Unlock; 
    property Handle : THandle read FHandle; end; 
    implementation 


    Глава 24. Работа с подзадачами 577 
    19 Использование Delphi 3. Специальное издание 


    constructor EMutexError.Create(aErrorCode : integer); begin 
    FErrorCode := aErrorCode; 
    inherited Create(Format('Win32 mutex error: %d', [aErrorCode])); end; 
    constructor TseMutex.Create(aName : string); begin 
    FHandle := CreateMutex(nil, false, PChar(aName)); 
    if (FHandle i""" б Г then""" 
    raise EMutexError.Create(GetLastError); end; 
    destructor TseMutex.Destroy; begin 
    if (FHandle <> 0) then CloseHandle(FHandle); end; 
    function TseMutex.Lock(aTimeOut : integer) : boolean; begin 
    Result := (WaitForSingleObject(FHandle, aTimeOut) <> WAIT_FAILED); end;    
    procedure TseMutex.Unlock; begin 
    ReleaseMutex(FHandle); end; 
    end. 

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

    Семафоры 

    Семафоры — это объекты синхронизации Win32, которые позволяют заранее 
определенном)' числу подзадач бороться за доступ, к ресурсам или данным. 
Семафор имеет счетчик, связанный с ним. Если его значение больше нуля, семафор 
находится в сигнализирующем состоянии и ожидающая подзадача начинает 
немедленно выполняться (при этом значение счетчика уменьшается). Когда 
значение счетчика становится равным нулю, остальные подзадачи переходят в 
состояние ожидания. Они будут ждать до тех пор, пока значение индикатора не 
станет снова положительным; в этом случае система выберет, какую подзадачу 
освободить, и значение индикатора семафора опять уменьшится на единицу. 
Значение индикатора увеличивается с помощью вызова функции Win32. К сожалению, 
вы не можете прочесть значение этого индикатора: Win32 его не показывает. 
    Представьте себе многопользовательскую сетевую игру, в которой число 
игроков не должно превышать четырех. Игровая программа отрабатывается на одной 
машине сети и использует некоторый протокол, подобный NetBIOS, чтобы передать 
сообщения по сети на другую играющую рабочую станцию. Для каждого игрока 
создается подзадача для управления продвижением по сети от этой рабочей 
станции и до обновления внутреннего содержания игры. Для этого, 
чтобы^г-раничить число игроков, вы можете создать семафор с начальным 
индикатором 4. Первые четыре созданныё™подзадачи смогут"запуститься. Значение 
индикатора семафора уменьшится до нуля, и семафор станет несигнализирующим. 
Подзадача следующего игрока будет ждать сигнала семафора до тех пор, пока один 
из активных игроков не прекратит игру и не увеличит тем самым значение 
индикатора семафора. 
    Семафоры, как события и мьютексы, глобальны для системы и, соответственно, 
должны быть названы уникальными именами. Замечу также, что чрезвычайно щюсто 
заменить семафоры использованием мьютексов и событий. Это вы можете сделать самостоятельно. 
    В листинге 24.6 представлена инкапсуляция семафора. 

    Листинг 24.6. \UDELPHI3\CHP24\SEMAPHORE. PAS. Инкапсуляция семафоров Win32 

    unit Semaphore; 
    interface 
    uses 


    578	Часть VI. Сложный материал 


    SysUtils, Windows; 
    type 
    ESemaphoreError = class(Exception) private 
    FErrorCode : integer; public 
    constructor Create(aErrorCode : integer); property ErrorCode : integer 
read FErrorCode; end; 
    TseSemaphore = class private 
    FHandle : THandle; protected public 
    constructor Create(aName : string; 
    alnitialCount : integer; aMaximumCount : integer); destructor Destroy; override; 
    function DecCount(aTimeOut : integer) : boolean; procedure IncCount; 
    property Handle : THandle read FHandle; end; 
    implementation 
    constructor ESemaphoreError.Create(aErrorCode : integer); begin 
    FErrorCode := aErrorCode; 
    inherited Create(Format('Win32 semaphore error: %d', [aErrorCode])); end; 
    constructor TseSemaphore.Create(aName : string; 
    alnitialCount : integer; aMaximumCount : integer); begin 
    FHandle := CreateSjsmajghore (nil, alnitialCount, aMaximumCount, PChar 
(aName) ); if (FHandle = 0) then' 
    raise ESemaphoreError.Create(GetLastError) ; end; 
    destructor TseSemaphore.Destroy; begin 
    if (FHandle <> 0) then CloseHandle(FHandle); end; 
    function TseSemaphore.DecCount(aTimeOut : integer) : boolean; begin 
    Result := (WaitForSingleObject(FHandle, aTimeOut) <> WAIT_FAILED); end; 
    procedure TseSemaphore.IncCount; begin 
    ReleaseSemaphore(FHandle, 1, nil); end; 
    end. 

    Как видите, это очень простой класс. Создав семафор, можете либо уменьшать 
(с перерывами, если требуется), либо увеличивать значение счетчика. Метод 
DecCount будет возвращать True в случае успешного уменьшения значения счетчика 
семафора. В противном случае он возвратит False, а это предполагает, что 
временной перерыв истек до уменьшения индикатора 

    Глава 24. Работа с подзадачами	579 


    Локальные данные подзадач: TLS и Threadvar 

    Если вы пишете код подзадачи, просто применяя функции BeginThread и 
EndThread, то может возникнуть ситуация, когда вы действительно должны будете 
так использовать глобальные переменные, чтобы они являлись локальными для 
каждой подзадачи. Проблема состоит в том, что все глобальные переменные видимы 
для всех подзадач, и, таким образом, вы должны как-то декларировать массив 
таких переменных (как много элементов) и так определить индекс для каждой 
подзадачи, которую вы создаете, чтобы конкретная подзадача могла просматривать 
и изменять элемент только с конкретным индексом. Это должен быть простой 
способ, и такой способ есть — использование локальной памяти подзадачи. 
    С помощью локальной памяти подзадачи (известной как TLS — Thread Local 
Storage) Win32 добивается, чтобы глобальные данные были различными для каждой 
подзадачи. Описать TLS легко (здесь нужно ознакомиться только с четырьмя 
подпрограммами), но с первого раза можно запутаться. Я знаю это по себе: я 
придерживался ложного представления о TLS до тех пор , пока тщательно не 
разобрался в этом вопросе (что и привело к написанию этой главы). 
    Win32 определяет битовый массив из 64 бит. Каждый раз при создании 
подзадачи Win32 создает массив из 64 указателей в стеке подзадачи и обнуляет 
их. Вы можете получить доступ к этому массиву, только используя подпрограммы 
TLS. Каждый бит в битовом массиве устанавливается для одного элемента в 
массиве указателей. Скажем, вы хотите получить указатель , котор.ый имеет 
различное значение для каждой подзадачи и к тому же является глобальным. 
Вообще говоря, указатель должен бы быть указателем на некоторого вида 
глобальную структуру данных. Сперва вы вызываете подпрограмму Win32 TlsAlloc, 
чтобы поместить индекс в массив указателей TLS: 

    function  TlsAlloc   :   DWORD; 

    Win32 исследует свой массив битов в поиске еще неустановленного. Если он 
найден, то он устанавливается, и возвращается индекс этого бита (если не 
найден, то возвращается код ошибки TLS_OUT_OF_INDEX). Этот индекс хранится в 
глобальной переменной (которую, естественно, могут видеть все подзадачи). Этот 
индекс относится к элементу массива указателей, который является частным для 
каждой подзадачи. К примеру, если TlsAlloc возвратила в качестве индекса 15, 
то каждая подзадача запишет затем индекс 15 в свой собственный частный массив 
для хранения этих данных и обращения к ним. 
    Если вы хотите сохранить значение для элемента скрытого массива указателей 
TLS, вызовите функцию TlsSet-Value следующим образом: 

    function  TlsSetValue   (dwTlsIndex   :   DWORD; 
    IpTlsValue   :   Pointer)    :   BOOL; 

    Можете передать индекс, который вы сохранили раньше (dwTlsIndex), и 
значение указателя, которое вы хотите сохранить (IpTlsValue). Win32 сохранит 
это значение в элементе частного массива, который задан этим индексом. Если бы 
другая подзадача хотела сохранить другое значение, то нужно было бы вызвать 
эту же подпрограмму, но в это время значение хранилось бы в копии массива TLS 
в стеке подзадачи. 
    Чтобы получить запись из TLS, вызовите TlsGetValue следующим образом: 

    function  TlsGetValue(dwTlsIndex   :   DWORD)    :   Pointer; 

    Вы можете передать индекс, который был сохранен после вызова TlsAlloc, как 
параметр dwTlsIndex. И вновь подпрограмма будет влиять на частный массив 
каждой подзадачи: различные подзадачи будут получать собственные локальные данные. 
    И, наконец, когда вы все это выполните, вызовите TlsFree, которая 
возвращает индекс в общий котел (чтобы сбросить биты в битовом массиве): 

    function TlsFree   (dwTlsIndex   :   DWORD)    :   BOOL; 

    Здесь параметр dwTlsIndex —это значение индекса, которое вы уже сохранили. 
    Поскольку данные, которые хранятся в TLS, — это указатели, вы обычно 
определяете б^и>цдто_структуру для хранения всех глобальных данных, которые вы 
хотите сделать частными и индивидуальными для каждой подзадачи, размещаете эту 
структуру в~кучё; а затем сохраняете указатель на нее в TLS. Если вы 
определили все это должным образом, то вами должен ис пользоваться только один 
из индексов TLS. Таким образом, тот факт, что существует 64 таких индекса, не 
должен слишком вас волновать. 
    Ну, достаточно теории. В листинге 24.7 приведен пример программы, которая 
использует TLS через интерфейс Win32. 

    Листинг 24.7. \UDELPHl3\CHP24\TestTLSRoutines. DPR. Проверка подпрограмм 
TLS Win32 

    program TestTLSRoutines; 
    ($APPTYPE CONSOLE} 
    uses 
    SysUtils, 


    580	Часть VI. Сложный материал 


    Windows, Padlock; 
    var 
    ConsolePadlock : TsePadlock; OurTlsIndex    : integer; 
    type 
    PThreadLocalData = PThreadLocalData; TThreadLocalData = record Number : 
integer; ID     : integer; end; 
    procedure SafeWriteLn(const S : string); begin 
    ConsolePadlock.Locked := True; try 
    writeln(S); finally 
    ConsolePadlock.Locked := False; end; end; 
    procedure WriteThreadNumber; var 
    TLD : PThreadocalData; begin 
    TLD := TlsGetValue(OurTlsIndex); 
    SafeWriteLn(Format('Thread ID %x is number %d', 
    [TLD.ID, TLD.Number])); end; 
    function MyThreadFunction(Parameter : pointer) : integer; var 
    TLD : PThreadLocalData; begin 
    Result = 0; New(TLD); try 
    TLD.Number := longint(Parameter) ; TLD.ID := GetCurrentThreadID; 
TlsSetValue(OurTlsIndex, TLD); WriteThreadNumber; finally 
    Dispose(TLD); end; end; 
    const 
    NumHandles := 20; 
    var 
    Handles  : array [0..pred(NumHandles)] of integer; ThreadID : integer; i   
     : integer; 
    begin 
    OurTlsIndex := -1; 
    FillChar(Handles, sizeof(Handles), 0); 
    ConsolePadlock := nil; 
    try 
    OurTlsIndex := TlsAlloc; 
    ConsolePadlock := TsePadlock.Create; 
    Handles[i] := BeginThread(nil, 0, MyThreadFunction, 


    Глава 24. Работа с подзадачами	581 


    Ч>	pointer(i),    0,   ThreadID); 
    WaitForMultipleObjects(NumHandles,    @Handles,   True,    INFINITE); finally 
    for  i   :=  0  to pred(NumHandles)   do if   (Handles[i]   <>   0)   then 
CloseHandle(Handles[i]); if   (OurTlsIndex   <>   -1)   then 
    TlsFree(OurTlsIndex   ) ConsolePadlock.Free; end; 
    writeln('Press   <Enter>  to  close...'); readln; end. 

    Основная программа размещает индексы TLS, вызывая TlsAlloc. Этот индекс 
хранится в глобальной переменной OurTlsIndex. Затем создается 20 подзадач и 
подпрограмма каждой подзадачи хранит некоторую информацию (ID подзадачи и 
номер создания подзадачи7~в"записи, размещенной в куче. Указатель на эту 
запись сохраняется в соответствующем индексе массива T 
    LS посредством вызова TlsSetValue и передачи для индекса переменной 
OurTlsIndex. Затем вызывается подпрограмма WriteThreadNumber. Эта подпрограмма 
может вызываться одновременно несколькими подзадачами, и ее целью является 
сообщение о месте хранения величин в записи, которую инициализирует каждая 
подзадача. Так что она узнает данные, вызывая TlsGetValue и передавая 
соответствующий индекс TLS, хранящийся в OurTlsIndex. 
    (Давайте все-таки внесем определенную ясность в этот довольно сложный для 
понимания материал: любую глобальную переменную сделать полностью локальной 
невозможно по определению. Таким образом, вам просто предоставляется механизм, 
с помощью которого вы можете создать собственные локальные переменные (и не 
более того), при котором система косвенной ссылки позволяет сделать глобальной 
лишь ссылку на ссылку (вернее, индекс ссылки). Глобальным в приведенном 
примере является только номер индекса TLS, который является зарезервированным 
во всех подзадачах одновременно, но даже указатель, получаемый с помощью этого 
индекса, каждая подзадача устанавливает по-своему. Таким образом, не 
происходит никакого автоматического создания локального двойника глобальной 
переменной, как может показаться. Необходимость в таком механизме практически 
возникает только в случае, когда одни и те же функции используются разными 
подзадачами и должны передать им данные, которые по тем или иньТм Причинам не 
могут быть переданы в качестве параметров. Например, в при веденном выше 
примере достаточно описать процедуру как procedure WriteThreadNumber (TLD: 
PThreadocalData) ; и передать ей из функции MyThreadFunction адрес ее 
локальной TLD, и проблема будет разрешена без излишних накладных расходов. 
(Понятно, что это так легко и просто в примере: в реальной жизни принимать 
решения о механизме передачи информации придется вам. — Прим. ред.) 
    Теперь, когда все оговорено, вы можете игнорировать подпрограммы TLS, 
поскольку вы в них не нуждаетесь. Причины? Резервное слово threadvar. Borland 
признает, что люди могут использовать локальные переменные подзадачи, и 
упрощает в Delphi их декларацию и использование. Ключевое слово threadvar 
работает почти так же, как и var, когда занимается глобальными переменными, с 
единственной разницей: переменные, декларируемые в блоке threadvar, являются 
локальными для каждой подзадачи. 
    Вы не должны волноваться по поводу вызовов TlsAlloc, TlsSetValue и т.п.: 
компилятор сам создает код, чтобы сделать все это. Компоновщик также 
включается в игру: он рассчитывает общий размер всех переменных threadvar и 
жестко привязывает к этому значению переменную в модуле System таким образом, 
чтобы блок памяти для хранения всех этих переменных можно было измерить и 
разместить во время выполнения программы. Компоновщик рассчитывает место для 
каждой описанной переменной threadvar во временном блоке памяти и вводит это 
значение в окончательный ЕХЕ-файл (или, может быть, в DLL-файл). После того 
как программа загружена, первичная подзадача выполняет подпрограмму 
инициализации, которая размещает для вас индекс TLS, размещает блок памяти для 
хранения всех переменных threadvar и устанавливает все это равным нулю. 
    Все_это происходит без какого бы то ни было участия с вашей стороны. Вы 
просто получаете удовольствие от использования переменных threadvar так же, 
как если бы вы использовали любую глобальную переменную. 
    Заметьте однако, что читать или записывать значение переменной threadvar 
по времени гораздо боле.е расточительно, чем значение локальной переменной или 
стандартной глобальной переменной (при этом приходится выполнять немалый 
фрагмент кода, созданный компилятором для обработки threadvar). Таким образом, 
если вы используете переменные threadvar в сдожных циклах (или даже несколько 
раз в одном и том же фрагменте кода), подумайте, не сделать ли локальную копию 
значения этой переменной, прежде чем начать цикл.	—"" ———•• ---.----• ............ 
    I Совет I 


    582	Часть VI. Сложный материал 

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

    | Листинг 24.8. \UDELPHI3\CHP24\TESTTHREADVAR. PAS. Сравнение приемов 
использования переменных ; threadvar и подпрограмм TLS 

    treadyar 
    ~~~DuгЙшпЬег : integer; OurlD     : integer; 
    // Код опущен. 
    procedure WriteTreadNumber 
    SafeWriteLn(Format('Thread ID %x is number %d', 
    [OurlD, OurNumber])); end; 
    function MyThreadFunction(P : pointer) : integer; begin 
    Result = 0; 
    OurNumber := longint(P); 
    ОмгЮ •.= GetCurrentTYixeadTD; 
    WriteThreadNumber; end; 

    Вот и все. Вы наверняка согласитесь, что все это — приятные мелочи в нашей жизни. 
    И все же, перед нами бесплатный сыр. А, где же мышеловка?.. 
    Конечно же, она есть, хотя и глубоко спрятана. Win32 определяет битовый 
массив так, чтобы он состоял из 64 бит. Другими словами, процесс может только 
разместить 64 индекса с помощью вызова в его подзадаче TlsAlloc. Вы говорите, 
что я декларировал переменные (одну или несколько) threadvar в приложении, а 
компилятор и компоновщик Delphi, действующие сообща, гарантируют, что будет 
использоваться только один индекс TLS? 
    Правильно, но то же самое относится и к библиотекам DLL Delphi. Библиотеки 
DLL в Delphi имеют свой собственный блок threadvar (в нем есть по меньшей мере 
две переменные, которые декларируются в модуле System). При загрузке 
динамические библиотеки образуют часть вызываемого процесса и затем, при их 
инициализации, используют индекс TLS. Таким образом, если ваше приложение 
единовременно загружает более 63 DLL, вы непременно столкнетесь с 
неприятно-стямиТТазмёщенный блок 64-й переменной threadvar будет испорчен 
(если в вашем приложении нет других подпрограмм, которые используют размещения 
TLS подобным же образом). Динамические библиотеки, которые построены с 
использованием других компиляторов, могут применять (и, вероятно, применяют) 
другие схемы размещения их статичных хранителей локальных переменных подзадач, 
но мы не будем касаться этих проблем. 

    Класс TThread 

    Теперь, быстро совершив круиз по этой главе и открыв для себя сущность 
подзадачи, мы внезапно наткнулись на класс Delphi TThread — класс, который 
инкапсулируют подзадачи. Все, о чем мы узнали во время нашего путешествия, 
позволяет состряпать легкое блюдо на основе этого класса, отказавшись от 
тяжелых мясных закусок — методов Execute и Synchronize. 
    Класс TThread был разработан для того, чтобы сгладить проблему 
одновременного доступа многих подзадач к VCL (особенно к визуальным 
компонентам и средствам управления). Говоря предельно коротко, библиотека VCL 
не является защищенной для подзадач. Если у вас есть несколько подзадач, 
обновляющих форму в одно время, то вы очень скоро получите сплошную мешанину и 
будете упрашивать некоего Access Violation все уладить. Корпорация Borland 
решила документировать тот факт, что VCL не является защищенной для подзадач, 
и обеспечить способ, которым_вы_можете подделать режим защищенности, а не 
тратить время, делая VCL защищенной. 
    По существу, этот фокус заключается в том, что вывод на экран может 
осуществлять только первичная подзадача. Когда подзадача, декларированная 
экземпляром TThread, захочет обновить экран, она вызывает подпрограмму (метод 
Synchronize), которая передает основной подзадаче сигнал о том, что требуется 
обновление экрана, а затем первичная подзадача выполняет эту работу. 
Методология, которую использует TThread, будет детально рассматриваться в 
следующих разделах. 

    Глава 24. Работа с подзадачами	583 

    Объявление TThread 
    Перед вами класс TThread, определенный в модуле Classes VCL. 

    TThread = class private 
    // Описания опущены, protected 
    procedure DoTerminate; virtual; procedure Execute;    virtual; abstract; 
procedure Synchronize(Method : TThreadMethod); property ReturnValue:Integer 
read FReturnValue 
    write FReturnValue; 
    property Terminated:Boolean  read FTerminated; public 
    constructor Create (CreateSuspended : Boolean); 
    destructor Destroy; override; 
    procedure Resume; 
    procedure  Suspend; 
    procedure  Terminate; 
    function  WaitFor : Integer; 
    property  FreeOnTerminate : Boolean read FFreeOnTerminate 
    write FFreeOnTerminate; 
    property Handle   : THandle read FHandle; property Priority : 
TThreadPriority read GetPriority 
    write SetPriority; 
    property Suspended: Boolean read Fsuspended 
    write SetSuspended; 
    property ThreadID : THandle read FThreadID; property OnTerminate : 
TNotifyEvent read FOnTerminate 
    write FOnTerminate; end; 
    Public 

    Сначала чуть-чуть посмакуем общий раздел. 

    Конструктор Create 

    Этот конструктор создает экземпляр класса TThread. Он увеличивает значение 
внутреннего счетчика подзадач (глобальная переменная), устанавливает пару 
внутренних полей объекта и затем вызывает BeginThread. Созданная подзадача 
оказывается в режиме ожидания, если параметр CreateSuspended равен True. 

    Деструктор Destroy 

    Этот деструктор разрушает экземпляр класса TThread. Если подзадача 
находится в работе, ее выполнение прекращается вызовом метода Terminate. 
Ожидание полного завершения подзадачи берет на себя метод WaitFor. Дескриптор 
подзадачи закрывается методом CloseHandle. После этого деструктор Destroy 
уменьшает значение внутреннего счетчика подзадач. 

    Метод Resume 

    Этот метод продолжает выполнение приостановленной подзадачи, вызывая ResumeThread. 

    Метод Suspend 

    Этот метод приостанавливает выполняемую подзадачу, вызывая SuspendThread. 

    Метод Terminate 

    Этот метод устанавливает внутренний флаг, чтобы показать, что подзадача 
будет прервана при первой же возможности. Этот флаг определяется свойством 
защиты Terminated. Метод Execute периодически проверяет флаг во время своей 
работы и прекращает работу подзадачи, если флаг принимает значение True. Если 
бы метод Execute не проверял флаг, то этот метод не произвел бы необходимого 
эффекта (и безусловно, подзадача не была бы остановлена, не имеет значения, 
сколько раз вы вызывали бы метод Execute!). 

    584	Часть VI. Сложный материал 


    Свойство FreeOnTerminate 

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

    Свойство Handle 

    Это доступное только для чтения свойство является дескриптором подзадачи и 
возвращается функцией Begin-Thread.	, ' 

    Свойство Priority 

    Это свойство, которое можно и читать, и записывать, является относительным 
приоритетом подзадачи, который устанавливается функцией SetThreadPriority и 
возвращается функцией GetThreadPriority. 

    Свойство Suspended 

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

    Свойство ThreadiD 

    Это доступное только для чтения свойство является уникальным 
идентификатором (ID) подзадачи, который возвращается BeginThread. 

    Событие OnTerminate 

    Это событие вызывается после того, как завершит работу метод Execute, но 
перед разрушением объекта TThread. На самом деле оно вызывается до того, как 
будет вызвана функция EndThread для подзадачи, и событие OnTerminate 
выполняется как часть собственно подзадачи. Чтобы избежать возможных проблем с 
VCL, программа обработки события OnTerminate вызывается через метод Synchronize. 

    Protected 
    Свойство DoTerminate 
    Вызывает программу обработки события OnTerminate через метод Synchronize. 
    Свойство ReturnValue 
    Код выхода подзадачи. EndThread будет вызываться с этим значением параметра. 
    Свойство Terminated 
    Это доступное только для чтения свойство имеет значение True, если 
выполнение подзадачи должно быть прекращено при первой возможности. Оно 
сохраняется вплоть до вызова метода Execute, который периодически проверяет 
этот флаг, и заканчивает выполнение подзадачи, если находит, что Terminated 
равно True. Этот флаг устанавливает метод Terminate. 

    Реализация TThread 

    Рассмотрим наиболее важные методы — методы класса TThread: Execute и Synchronize. 

    Execute 

    Этот метод осуществляет выполнение подзадачи. В классе TThread он 
определен как виртуальный (virtual) и абстрактный (abstract). Другими словами, 
TThread — это абстрактный класс, и вы не можете создать его экземпляра и 
ожидать, что он будет работать (вы получите ошибку выполнения). Это означает, 
что вы должны создать класс — потомок TThread, который,.переопределяет 
Execute. Метод Execute будет работать внутри той подзадачи, которую создаст 
конструктор Create. 

    Synchronize 

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

    Глава 24. Работа с подзадачами	585 

    ку извещающее сообщение отсылается при использовании подпрограммы Win32 
SendMessage (сравните с PostMes-sage), Synchronize ждет вызова экранного 
метода. Результатом является то, что подзадача будет заморожена вплоть до 
полной отработки экранного метода (экранный метод выполняется внутри первичной 
подзадачи). Это, конечно, не on-т^ма^м1Ь1Елодхсш^др ^днп91,,)крайней 
мере,означает, что вы не должны использовать.Рас11о?^или,лент Л.одобное для 
последовательного доступа к внутренним полям объекта вашей подзадачи. 
^Л"2!:^:^Р"'} Достаточно теории. Сейчас самое время написать маленький пример 
класса— потомка TThread, который :!I-'V.?•"'•',,.,.Я покажет, как использовать 
Execute и Synchronize. Работа, которую будет выполнять подзадача, заключается 
в том, чтобы архивировать/разархивировать файлы с помощью метода 
разветвляющегося дерева. Я не WWW ^УДУ описывать особенности этого алгоритма 
сжатия, скажу только, что он чрезвычайно прост и сжимает информацию не так 
сильно, как метод Zip или LZW (к примеру, текстовые файлы сжимаются до 66% от 
их первоначального размера). Кроме того, этот алгоритм, конечно, не относится 
к теме этой главы. Для получения более детальной информации обратитесь к 
статье Джона Дугласа (W. Jones Douglas) "Application of Splay Trees to Data 
Compression" ("Применение метода разветвленных деревьев для сжатия данных", 
Communication of the ACM, август 1988, стр. 996). Я пользуюсь вариантом 
программы на Pascal, написанной Кимом Кокконеном (Kirn Kokkonen), 
TurboPowerSoftware, и вы можете получить исходный SPLAY. LZH по адресу http: 

    //www.turbopower.com. 

    Как бы то ни было, вернемся к коду подзадачи. Такая подзадача будет либо 
архивировать, либо распаковывать файл. Она будет показывать ход выполнения 
процесса с помощью индикатора в основной форме. В действительности процессами 
упаковки и распаковки будет заниматься метод Execute, тогда как за обновление 
индикатора (progress bar) возьмется метод Synchronize. 
    Для того чтобы создать основное приложение, выполните следующие действия. 
    1.   Создайте проект. 
    2.   Поместите в форму кнопку (Tbutton — в палитре Standart) и три линейки 
индикатора работы (TProgressBar — в палитре Win95). Задайте надпись кнопки 
Start и ее имя — StartButton. Кроме того, разместите на форме три статичных 
элемента управления (TLabel) для того, чтобы надписать каждую линейку 
продвижения. Порядок размещения показан на рис. 24.1. 
    Мы будем работать со всеми индикаторами хода выполнения одновременно, и 
каждый из них будет показывать ход выполнения одной подзадачи. 

    Рис. 24.1. Форма для демонстрации работы TThread 

    3.   Создайте новый объект подзадачи (выполните команду File^New и 
выберите Thread Object из списка пиктограмм). Назовите этот класс 
TSplayCompressThread. Сохраните проект (File^Save All), назвшГмодуль Unitl 
Main, модуль Unit2 — SplayCompressThread, а файл проекта Projectl — DemoTThread. 
    4.   В модуле SplayCompressThread определите класс TSplayCompressThread, 
чтобы включить в него некоторые новые поля, свойства и методы. 

    TSplayCompressThread =  class(TThread) private 
    {   Private  declarations   } 
    FInFileName      :   string; 
    FOutFileName   :   string; 
    FDoCompress      :   boolean; 
    FPercentDone   :   integer; 
    FProgressBar   :   TProgressBar; protected 
    procedure  PercentDoneNotifу(Percent   :   integer); 
    procedure   DisplayPercentDone; public 
    constructor  Create   (InFileName,   OutFileName   :   string; Decompress  
 :   boolean); 
    procedure Execute	:   override; 
    property PercentDone   :   integer  read FPercentDone; 
    property  ProgressBar   :   TProgressBar write  FProgressBar; end; 


    586 Часть VI. Сложный материал 

    5.   Введите следующий фрагмент реализации конструктора Create в этом же модуле. 

    constructor TSplayCompressThread.Create(InFileName, 
    OutFileName   :   string;   Decompress   :   boolean); begin 
    inherited Create   (True); FInFileName   :=  InFileName; FOutFileName   
:=OutFileName; FDoCompress   :=   Decompress; FPercentDone   :=  0; end; 

    Здесь ничего особо интересного нет. Все параметры хранятся во внутренних 
полях. Метод предка Create вызывается со значением True параметра 
CreateSuspended, другими словами, эта подзадача будет создана в 
приостановленном состоянии. 
    6.   Введите следующий фрагмент реализации метода Execute.... 

    procedure TSplayCompressThread.Execute; var 
    SCE : TSplayCompressThreadEngine; begin 
    SCE := TSplayCompressThreadEngine.Create (FInFileName, FOutFileName); 
    SCE.PercentDone := PercentDoneNotify; 
    if FDoCompress then SCE.Compress 
    else 
    SCE.Expand 
    SCE.Free end; 

    Этот метод создает экземпляр класса TSplayCompressThreadEngine. Он 
назначает свойство PercentDone этого класса своему собственному методу 
PercentDoneNotif у. Таким образом, PercentDoneNotif у будет вызываться 101 
раз: по одному разу для каждого процента от 0 до 100 включительно. После этого 
метод Execute вызывает программу сжатия или распаковки вх одного файла. На 
последнем этапе разрушается объект упаковщика. 
    7.   Введите следующий фрагмент реализации метода PercentDoneNotif у. 

    procedure  TSplayCompressThread.PercentDoneNotifу(Percent   :integer); begin 
    FPercentDone   :=   Percent; 
    Synchronize   (DisplayPercentDone); end; 

    Этот метод вызывается упаковщиком всякий раз при изменении процента 
выполнения архивирования (или разархиви-рования) файла. Он вызывает метод 
Synchronize, передавая ему метод DisplayPercentDone. 
    8.   Введите следующий фрагмент реализации метода DisplayPercentDone. 

    procedure  TSplayCompressThread.DisplayPercentDone; begin 
    FProgressBar.Position   :=   FPercentDone; end; 

    Как видите, эта часть кода только обновляет индикатор работы, связанный с подзадачей. 
    Итак, класс-потомок создан и уже понятно, как работают методы Execute и 
Synchronize. Метод Execute создает и запускает объект, занятый собственно 
сжатием информации, освобождая его после того, как работа будет закончена. 
Метод Synchronize будет вызываться всякий раз, когда необходимо обновить 
индикатор выполнения операции, и то, что делает в действительности этот метод, 
вызывает метод DisplayProgressDone. 
    Теперь "нарастим мясо" на класс TForml в модуле Main. 
    9.   В разделе private класса TForml добавьте следующие поля. 

    TerminatedCount   :   integer; Threadl   :   TSplayCompressThread; Thread2 
  :   TSplayCompressThread; Threads   :   TSplayCompressThread; 


    Глава 24. Работа с подзадачами	587 

    10. Добавьте следующее определение метода в раздел public этого же класса формы. 
    procedure ThreadTerminated (Sender : TObject); 
    11. Реализуйте метод ThreadTerminated. 

    procedure  TForral.ThreadTerminated   (Sender   :   Tobject); begin 
    inc   (TerminatedCount); if   (TerminatedCount  =  3)   then begin 
StartButton.Enabled   :=  true; ProgressBarl.Position   := 0; end; end; 

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

    procedure  TForml.StartButtonClick   (Sender   :   Tobject); begin 
    StartButton.Enabled   :=  false; 
    TerminatedCount   :=  0; 
    Threadl   := TSplayCompressThread.Create('\8-73.HTML1, 'Testl.SCF',   true); 
    Thread2   := TSplayCompressThread.Create('\8-73.HTML*, 'Test2.SCF',   true); 
    Threads   := TSplayCompressThread.Create('\8-73.HTML', 'Test3.SCF',   true); 
    Threadl.ProgressBar   := ProgressBarl; 
    Threads.ProgressBar   :=  ProgressBar2; 
    Threads.ProgressBar   :=  ProgressBarS; 
    Threadl.FreeOnTerminate   := true; 
    Thread2.FreeOnTerminate   :=  true; 
    Threads.FreeOnTerminate   :=  true; 
    Threadl.OnTerminate   :=  ThreadTerminated; 
    Thread2.0nTerminate   := ThreadTerminated; 
    Threads.OnTerminate   := ThreadTerminated; 
    Threadl.Resume; 
    Thread2.Resume; 
    Threads.Resume; end; 

    Этот метод делает недоступной кнопку Start, устанавливает значение 
счетчика законченных подзадач равным нулю и после этого создает подзадачи. В 
нашем случае имеется всего три подзадачи, и все они занимаются сжатием 
большого текстового файла, каждая— в свой файл (в нашем случае этот большой 
текстовый файл называется '\8-73.HTML', так как он был у меня под рукой на 
моем домашнем компьютере, когда я проверял этот пример; вы можете использовать 
любое имя файла, какое вам только нравится (чем больше файл, тем лучше), чтобы 
одновременно видеть работу всех трех подзадач). Каждому объекту подзадач 
задается свой индикатор хода выполнения, каждый из них отображает степень 
готовности до момента их разрушения, когда завершается упаковка, и 
непосредственно перед тем как это произойдет каждая их них вызывает метод этой 
формы ThreadTerminated. В конечном итоге каждая подзадача возобновляет свою 
работу (помните, что подзадача создавалась в приостановленном состоянии?). 
    13. Откомпилируйте и запустите приложение на выполнение. Щелкните на 
кнопке Start, и подзадачи сожмут данные и обновят свои собственные индикаторы 
хода выполнения. 

    Недостатки TThread 

    Недостатки? Это еще что такое? Боюсь, что они все же существуют... 
Программист корпорации Borland, который разрабатывал TThread, неумело 
обращался со своей добычей. Разберемся в этом. 
    Когда создается экземпляр TThread, внутренняя подпрограмма начинает 
"крутиться по хозяйству". Она сохраняет активным счетчик количества 
экземпляров TThread. Если это первый экземпляр TThread, то ссылка на окно 
создается таким образом, чтобы метод Synchronize мог куда-нибудь отсылать 
сообщения. Подобным же образом, когда послед

    588	Часть VI. Сложный материал 

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

    if ThreadCount  =  0  then 
    ThreadWindow   := AllocateWindow; Inc   (ThreadCount); 

    А подпрограмма разрушения выглядит так. 

    Dec   (ThreadCount}; 
    if ThreadCount  =  0  then  DestroyWindow(ThreadWindow}; 

    Немного поломав над этим голову, вы разглядите следующую проблему. 
Предположим, что подзадача создается на основе древнейших законов — с помощью 
BeginThread. Обе подзадачи (и эта, и первичная) могут создавать экземпляры 
TThread. Легко увидеть, что может случиться так, что последний экземпляр 
TThread будет разрушен в одной подзадаче, тогда как в другой будет создан 
новый экземпляр TThread. При правильном многозадачном переключении в 
действительно правильное время может быть создан дескриптор нового окна (а 
старое выгонят и оставят беспризорным), который будет потом разрушен легким 
взмахом волшебной многозадачной палочки. (Вообразите себе, что подпрограмма в 
первой подзадаче уменьшила значение счетчика подзадач до нуля; Win32 
переключится на дополнительную подпрограмму, работающую с другой подзадачей, 
которая затем создаст новое окно. Win32 вновь переключается на подпрограмму, 
которая разрушает только что созданное окно. А Win32 опять переключается на 
дополнительную подпрограмму, чтобы увеличить значение счетчика.) Даже если 
произойдет только одно переключение, вы получите беспризорное окно, напрасно 
потребляющее ресурсы. Маловероятно, но возможно. Это, безусловно, можно было 
предотвратить с помощью критического раздела или документированно сообщив вам, 
что если вы используете объекты TThread, то по крайней мерине создавайте 
подзадачи, используя BeginThread. 
    Если вы ничего не уяснили из этой главы, с вашей программой с подзадачами 
может случиться что угодно. 
    Случайно, конечно... 

    Другие темы для изучения 

    О некоторых вопросах в этой главе мы даже не упомянули. В основном так 
получилось из-за того, что у меня было очень мало места для главы, ну и еще 
из-за того, что, дабы полностью воздать должное приемам многозадачного 
программирования, понадобилась бы целая книга (такая, как эта). 
    Ниже рассматривается пара вопросов, которые вам не мешало бы изучить и 
использовать на практике. 
    •   Win32 поддерживает асинхронный ввод-вывод файла. Этот прием 
используется приложением, которое требует, чтобы операционная система читала 
или писала (что не имеет значения) информацию из файла и продолжала выполнять 
некоторый процесс одновременно с вводом-выводом файла. Вообще говоря, вы могли 
бы создать подзадачу, которая осуществляла бы доступ к файлу, и заставить ее 
ждать, пока считываются данные; дескриптор файла сигнализировал бы об 
окончании операции ввода-вывода. Асинхронный ввод-вывод файла чаще всего 
используется для передачи данных в программах связи по последовательным 
портам. Фактически Microsoft рекомендует в подобной ситуации использовать 
именно этот прием.	, 
    •   Windows NT 4.0 ввела новый объект — фибер. Фибер (fiber — волокно) — 
это нечто, чуть меньшее, чем подзадача (как можно было бьГдогадаться по 
переводу). Это упрощенные подзадачи (а подзадачи можно назвать упрощенными 
процессами). Использование подзадач приводит к некоторым дополнительным 
расходам и требует слишком много времени для переключения между подзадачами, 
тогда как фиберы являются индивидуальной ниточкой выполнения внутри одной 
подзадачи и не требуют дополнительных расходов. Фиберы имеют свой собственный 
стек и небольшой информационный контекст. Windows NT 4.0 не поддерживает 
вытесняющего режима многозадачности на уровне фиберов; этот режим предназначен 
только для подзадач, что и ограничивает использование фиберов, так как 
необходимо вручную переключать программу подзадачи между фи-_ бёрамй. В третье 
издание Джеффри Ричтера (Jeffrey Richter) Advanced Windows (Microsoft Press) 
включен раздел о фиберах. 

    Глава 24. Работа с подзадачами	589 


    Резюме 

    В этой главе речь шла о подзадачах. Они позволяют приложению создать 
иллюзию одновременного выполнения нескольких задач. Эта возможность обязывает 
нас быть уверенными, что применение подзадач сделает приложение более гибким, 
а не использовать подзадачи только потому, что мы знаем, как это делается, и 
чувствуем себя программистами без страха и упрека. 
    Вы узнали, как создавать и использовать подзадачи, синхронизировать 
созданные подзадачи, как удостовериться, что процесс доступа к данным является 
управляемым и исключает любой риск, и почему все это так важно. Здесь 
определено несколько синхронизирующих классов для последовательного доступа к 
глобальным данным. Кульминацией стало описание сложного синхронизирующего 
класса, необходимого для доступа подзадач к глобальным данным. Мы коснулись 
также проблемы взаимной блокировки. 
    В данной главе рассказывалось о классе TThread, его применении и недостатках. 
    Одним словом, подзадачи — это здорово! Они заботятся о вас и планируют 
работу в ваших интересах. И когда план согласованный, это впечатляет. 
    Очень впечатляет! 

    590	Часть VI. Сложный материал 

