В предыдущей главе было рассмотрено создание сервера автоматизации, выполненного в виде исполняемого файла. Однако нередко сервер автоматизации размещается в динамически загружаемой библиотеке - DLL. При обращении к такому серверу происходит загрузка библиотеки (если ранее она не была загружена) и запуск вызываемого метода.
Главное отличие сервера автоматизации в DLL заключается в том, что все объекты, содержащиеся в DLL, находятся в адресном пространстве приложения, которое к ним обращается. Поэтому сервер автоматизации, расположенный в DLL, называют внутренним или in-process-сервером, в то время как сервер, расположенный в исполняемом файле, называют локальным (out-of-process) сервером.
Внутренний сервер может манипулировать объектом, который был создан процессом в исполняемом файле, если на него передан указатель. Локальный сервер при попытке использования указателя на объект, созданный другим процессом, в лучшем случае сгенерирует сообщение об ошибке Access Violation. Для передачи указателя на объект, созданный другим процессом, необходимо использовать интерфейс IMarshall, который находит указатель на объект, созданный одним процессом в адресном пространстве другого процесса. Подробнее об IMarshall рассказывается в разделе 1.7.
Создание такого сервера всегда начинается с создания библиотеки ActiveX. Для этого необходимо выполнить команду "File/New". Затем необходимо выбрать страницу ActiveX из репозитария и на этой странице выбрать пиктограмму ÄctiveX library" (как это делалось при создании элементов управления ActiveX). Встроенный эксперт создаст новый проект. Этот проект будет генерировать код для создания DLL, при этом внешним приложениям будут видны четыре метода.
HRESULT DllGetClassObject (REFCLSID rclsid, REFIID riid, LPVOID *ppv); HRESULT DllCanUnloadNow (void); HRESULT DllRegisterServer (void); HRESULT DllUnregisterServer (void);
Функция DllGetClassObject создает фабрику класса CLSID и возвращает уникальный идентификатор интерфейса IID в переменной ppv приложению, вызвавшему эту функцию. Функция DllCanUnloadNow вызывается системой для того, чтобы установить, возможно ли выгрузить DLL из памяти и вернуть ресурсы системе, или нет (если данную DLL используют какие-либо процессы). Наконец, две последние функции используются для записи данных о сервере в системном реестре и для удаления них записей оттуда.
Затем следует вновь обратиться к пункту меню "File/New" и на странице ActiveX репозитария выбрать пиктограмму Äutomation Object". Так же как и в случае локального сервера автоматизации, появится диалог, в котором надо определить имя класса и способ создания фабрики класса при создании СОМ-объекта - будет ли создаваться новая копия (single instance), или будет возвращаться ссылка на имеющуюся копию (multiple instance). Поскольку одна DLL может обслуживать несколько приложений, выберем значение Multiple Instance. Далее следует заполнить еще два поля:
После заполнения всех полей диалога будет создана библиотека типов и модуль, где необходимо создать реализацию конкретных методов, также как и для локального сервера автоматизации. В библиотеке типов создается интерфейс, в котором следует определять свойства и методы, а так же диспинтерфейс, если реализуется поддержка нотификационных сообщений. Далее можно определить новые свойства и методы, так же как это было сделано при создании локальных серверов автоматизации.
Сервер автоматизации необходимо регистрировать в системном реестре. При этом нужно вызвать определенную ранее функцию DLLRegisterServer из какого-либо приложения. Ниже приведен фрагмент кода:
typedef HRESULT (__stdcall *DLLSERVER) (void); ... void __fastcall TForm1::Button1Click(TObject *Sender) { HINSTANCE hLib; DLLSERVER RServ; hLib = LoadLibrary (Edit1->Text.c_str ()); if (!hLib) { ShowMessage ("Не могу найти библиотеку"); return; } RServ = (DLLSERVER) GetProcAddress (hLib, "DllRegisterServer"); if (RServ) { if (RServ () == S_OK) { ShowMessage ("Выполнена регистрация сервера."); } else { ShowMessage ("Ошибка при регистрации сервера."); } } else { ShowMessage ("Не могу найти метод DllRegisterServer"); } FreeLibrary (hLib); }
В операционной системе Windows имеется приложение regsvr32.exe. При помощи этого приложения можно регистрировать внутренние серверы автоматизации в системном реестре и, наоборот, удалять сведения о них из реестра. Ниже приведены примеры соответствующих команд:
regsvr32 axstrcnt.dll - регистрация сервера regsvr32 /u axstrcnt.ocx - удаление сервера из системного реестра
Все сказанное в этом разделе применимо не только к серверу автоматизации, но и при вызове любой DLL. Следует обратить внимание на то, что DLL и приложение, которое ее вызывает, имеют независимые менеджеры памяти (memory manager). Поэтому, если память выделяется в приложении, абсолютно недопустимо освобождать ресурсы в DLL, и наоборот, если память выделяется в DLL, недопустимо высвобождать ресурсы в приложении.
Типичный пример вызова DLL приведен ниже:
int i, N; int *Data; Data = new int[1000]; N = CalculateSum (Data, 1000); delete Data;
Соответственно в DLL имеется метод:
int CalculateSum (int *pData, int Count) { int s = 0; for (int i = 0; i < Count; i++) { s += pData[i]; } return s; }
В приведенном примере следует обратить внимание на то, что методы new и delete находятся в вызывающем приложении. Попытка использовать delete в DLL приводит к ошибке Access Violation. В данном примере ресурсы выделяются в приложении и затем используются в DLL. Если же необходимо поступить наоборот, то есть выделить ресурсы в DLL, а затем использовать их в приложении, то в DLL необходимо предусмотреть вызов дополнительного метода для освобождения ресурсов. Типичная реализация выглядит следующим образом:
void CreateData (int *OutData) { OutData = GlobalAlloc (0, 1000 * sizeof (int)); for (int i = 0; i < 1000; i++) { OutData[i] = i; } } void ReleaseData (int *OutData) { if (OutData) { GlobalFree (OutData); } }
Оба эти модуля помещаются в DLL, основное приложение сначала вызывает метод CreateData, а после окончания работы с ресурсами обязано вызвать метод ReleaseData.
Понятие многопоточности тесно связано с возможностью выполнения нескольких приложений одновременно. Еще в Windows 3.1 была реализована такая возможность. Однако если какое-либо приложение выполняло долговременные операции, то до окончания вычислений пользователь не мог обратиться к другим приложениям. Такая ситуация называется захватом процессора (processor capture).
В Windows 95 и Windows NT уже была реализована так называемая "истинная многозадачность. Ее идея заключается в следующем: спустя определенный в системе квант времени запоминаются состояния регистров процессора и затем они заменяются данными для другой задачи. Далее вычисления продолжаются для новой задачи. Когда вновь наступает очередь выполнения первой задачи, содержимое регистров процессора восстанавливается и продолжаются вычисления.
Соответственно возникает вопрос: а можно ли в рамках одного приложения запустить несколько задач? Действительно, получается удобный пользовательский интерфейс: приложение печатает отчет, одновременно проводит статистическую обработку данных и еще при этом дает возможность пользователю просматривать данные. Такой тип вычислений называют многопоточным (multi-threading). Русский перевод этого термина не очень удачен, так как потоки вычислений можно перепутать с потоками данных (streams). В этой главе речь будет идти только о потоках вычислений (threads).
При запуске любого приложения всегда создастся один поток вычислений, который называют главным. Далее приложение может создать дополнительные потоки вычислений. Наиболее просто это можно сделать создав объект TThread. Метод Execute, определенный и таком объекте, будет выполняться уже в другом потоке. Таких объектов может быть создано несколько, но не рекомендуется для одного приложения создавать их более шестнадцати.
Одно из важнейших свойств потоков вычислений - их приоритетность. Приоритетность определяет, сколько квантов времени будет выделяться процессу. Чем выше приоритетность, тем больше таких квантов времени будет выделяться такому потоку. Приоритетность определяется относитсльно приоритетности главного потока и может принимать значения tpIdle, tpLowest, tpLower, tpNormal, tpHigher, tpHighest, tpTimcCritical. Последний тип приоритетности применяется только в специальных приложениях. Дело и том, что многие методы ядра Windows представляют собой асинхронные процессы с приоритетом tpHighest. Это значит, что если будут вызваны некоторые методы Windows API, то такое приложение захочет выполнять задачу дальше, не дожидаясь окончания их выполнения, что, как правило, приводит к генерации исключения.
Программист никогда не должен предполагать, что какой-либо поток завершит свою работу раньше другого. Казалось бы, если два потока производят примерно поровну вычислений и были запущены одновременно с приоритетами tpLowest и tpHighest, то следует ожидать, что второй поток завершится раньше, и заманчиво попытаться использовать результаты его вычислений в финальной стадии вычислений первого потока. Но на компьютере с несколькими процессорами при малом числе запущенных приложений эти потоки будут выполняться на разных процессорах со 100-процентным предоставлением времени каждому из них.
Таким образом, мы приблизились к понятию синхронизации. Предположим, имеется два потока: один считывает показания датчиков, формирует массив данных и заносит данные в память компьютера, а другой использует эти данные для формирования диаграммы и отображает ее на мониторе. Каждый из этих потоков может прервать работу другого в любой момент. Если произойдет прерывание первого потока в момент занесения данных, то часть массива будет с новыми данными, а часть - со старыми.
Понятно, что первому потоку следует запретить заносить данные в глобальные переменные, пока второй поток их считывает, и наоборот, если второй поток читает эти данные, то первый должен подождать с их формированием. Такое поведение двух потоков называется синхронизацией. Объект TThread имеет метод Synchronize, который позволяет синхронизировать главный поток приложения с потоком, созданным данным объектом. При вызове какой-либо процедуры через этот метод главный поток приостанавливает свою работу и дожидается окончания работы процедуры, вызванной в методе Synchronize.
Однако метод Synchronize неприменим, если необходимо синхронизировать работу двух неосновных потоков. Для их синхронизации используются семафоры, мьютексы и критические секции. В данной главе будут обсуждаться только критические секции.
В ряде случаев в приложении желательно организовать несколько потоков (нитей), выполняющихся одновременно. Например, одна нить выполнения может осуществлять основную работу, а вторая, с меньшим приоритетом, может в то же время готовить или реорганизовывать какие-то файлы, рисовать изображения, которые потребуются в дальнейшем, т.е. выполнять черновую работу. Другой пример - параллельная работа с несколькими внешними источниками информации. Особенно большой выигрыш в производительности за счет параллельного выполнения нескольких нитей можно получить в многопроцессорных системах, в которых можно организовать выполнение каждой нити отдельным процессором.
Параллельно выполняемые нити работают в адресном пространстве одного процесса и могут иметь доступ к глобальным переменным этого процесса.
Одним из способов создания приложения с несколькими потоками является использование компонента типа TThread. Этот компонент отсутствует в палитре библиотеки. TThread - это абстрактный класс, позволяющий создать в приложении отдельную нить выполнения. Для того чтобы ввести TThread в свое приложение, надо выполнить команду \"File New Other\" и в открывшемся окне репозитария на странице "New" выбрать пиктограмму "Thread Object". Вам будет задан вопрос об имени (Class Name) создаваемого класса, наследующего TThread. Укажите любое имя (например, Т) и в ваш проект добавится новый модуль, файлы которого имеют вид:
// заголовочный файл Unit2.h: #ifndef Unit2H #define Unit2H #include <Classes.hpp> class T : public TThread { private: /* Добавляемые здесь и в разделе protected свойства и методы будут * доступны только внутри класса */ protected: void _ fastcall Execute(); public: _ fastcall Т (bool CreateSuspended); /* Добавляемые здесь свойства и методы будут доступны * для функций приложения через объект нити */ } #endif // файл реализации Unit2.cpp: #include <vcl.h> #include "Unit2.h" // Important: Methods and properties of objects in VCL // can only be used in a method // called using Synchronize, for example: // Synchronize (UpdateCaption); // where UpdateCaption could look like: // void __fastcall Unit2::UpdateCaption () // { // Form1->Caption = "Updated in a thread"; // } /* Важно: методы и свойства объектов VCL могут использоваться * только с применением метода, который вызывается методом * Synchronize, например * Synchronize (UpdateCaption); * где метод UpdateCaption может иметь вид: * void __fastcall Unit2::UpdateCaption () * { * Form1->Caption = "Updated in a thread"; * } */ __fastcall T::T(bool CreateSuspended) : TThread (CreateSuspended) { } void __fastcall T::Execute() { // Place thread code here // Здесь размещается код потока }
В приведенный выше текст добавлен перевод комментариев, которые С++Builder помещает в модуль, а также введены комментарии, поясняющие область видимости вводимых вами новых свойств и методов.
Созданный C++Builder модуль, как вы можете видеть, содержит заготовку класса с введенным вами именем (в нашем пример - Т), наследующего TThread. Вы можете добавлять в него любые свойства и методы, учитывая отмеченные В комментариях области видимости. Процедура Execute, заготовку которой вы можете видеть в коде, является основной процедурой нити. При ее окончании завершается и выполнение данной нити приложения.
Класс наследует от TThread ряд методов. Прежде всего это конструктор, создающий объект нити:
__fastcall TThread(bool CreateSuspended);
Параметр CreateSuspended конструктора определяет способ выполнения нити. Если CreateSuspended = false, то выполнение процедуры Execute начинается немедленно после создания объекта. Если CreateSuspended = true, то выполнение начнется только после того, как будет вызван метод Resume:
void __fastcall Resume (void);
Конструктор TThread не должен вызываться в приложении явным образом. Для создания объекта класса TThread, как и для всех классов VCL, надо использовать операцию new. Например:
Т *SecondProcess = new T(true); SecondProcess->Resume();
Функция Resume имеет две области применения. Во-первых, она запускает выполнение, если объект нити был создан с CreateSuspended = true. Во-вторых, она запускает приложение, приостановленное ранее методом Suspend:
void __fastcall Suspend(void);
Таким образом, вы можете в любой момент приостановить выполнение нити методом Suspend, а затем продолжить выполнение методом Resume. Если вы подряд несколько раз вызвали метод Suspend, то выполнение возобновится только после того, как столько же раз будет вызван метод Resume. Узнать, является ли нить приостановленной, можно по значению булева свойства Suspended.
В функции Execute можно непосредственно писать операторы выполнения, вызовы каких-то иных функций и т.п. Однако если функция должна вызывать какие-то методы компонентов VCL или обращаться к свойствам формы, то необходимо соблюдать осторожность, поскольку при этом не исключены конфликты между параллельно выполняемыми нитями. В этом случае в функции надо вызывать метод Synchronize, как сказано в комментарии, который вы могли видеть в приведенной выше заготовке модуля. Метод определен следующим образом:
typedef void __fastcall (__closure *TThreadMethod) (void); void __fastcall Synchronize (TThreadMethod &bMethod);
В этом определении Method - функция, работающая с компонентами VCL. Таким образом, при работе с компонентами VCL надежнее строить выполнение следующим образом. Вы пишете функцию, выполняющую необходимые действия с компонентами VCL. Пусть вы дали ей имя Work. Тогда вы включаете ее объявление в класс нити, например, в раздел private, даете в файле реализации ее описание, а процедура Execute в этом случае может, например, состоять из единственного оператора Synchronize(Work):
// заголовочный файл Unit2.h class Т : public TThread { private: void __fastcall Work(void); protected: void __fastcall Execute(); public: __fastcall T (bool CreateSuspended); }; // файл реализации Unit2.cpp void __fastcall T::Execute() { Synchronize (Work); } void __fastcall T::Work(void) { ... }
При завершении процедуры Execute происходит нормальное завершение выполнения нити. Однако возможно и досрочное завершение выполнения нити. Для этого в ее процедуру Execute должна быть введена проверка булева свойства Terminated (завершено). Нормально это свойство равно false. Но если какая-то внешняя нить вызвала метод Terminate объекта данной нити, то Terminated становится равным true. Если предполагается возможность такого завершения выполнения нити, то процедура Execute должна периодически проверять значение Terminated и при получении значения true должна завершаться. Например:
void __fastcall T::Execute() { do { ... } while (!Terminated); }
Метод Terminate обеспечивает "мягкое" завершение нити. Процедура Execute сама решает, в какой момент ей удобно завершить выполнение. Имеется и более грубая функция API Windows - TerminateThread, вызывающая немедленное завершение выполнения нити. Например, оператор
TerminateThread ((void *)SecondProcess->Handle, 0);
прервет выполнение объекта нити с именем SecondProcess. В этом операторе использовано свойство Handle (дескриптор) нити, позволяющее обращаться к функциям API Windows. Второй параметр функции TerminateThread в приведенном примере 0) задает код завершения нити. Этот код можно прочитать в свойстве ReturnValue объекта нити.
Что именно происходит при завершении выполнения нити, определяется свойством FreeOnTerminate объекта типа TThread:
__property bool FreeOnTerminate
Если FreeOnTerminate = true, то при завершении выполнения объект TThread разрушается, освобождая память. При FreeOnTerminate = false освобождать память надо явным применением к объекту операции delete.
Если одновременно выполняется несколько нитей, то может потребоваться, чтобы какая-то нить ожидала завершения другой нити, которая, например, готовит ей исходные данные. Такое ожидание можно осуществить, применив к нити, завершения которой надо ждать, метод WaitFor:
int __fastcall WaitFor (void);
Этот метод возвращает свойство ReturnValue - код завершения ожидаемой нити. Метод не возвращается, пока нить, которую ожидают, не завершит выполнения.
Каждая выполняемая нить имеет определенный уровень приоритета, определяемый значением ее свойства Priority. Это свойство может иметь следующие значения:
tpIdle | Нить выполняется только когда система свободна. |
tpLowest | Приоритет нити на два пункта ниже нормального. |
tpLower | Приоритет нити на один пункт ниже нормального. |
tpNormal | Нормальный приоритет нити. |
tpHigher | Приоритет нити на один пункт выше нормального. |
tpHighest | Приоритет нити на два пункта выше нормального. |
tpTimeCritical | Нить имеет наивысший приоритет. |
Мы рассмотрели все свойства и методы объекта-наследника TThread. Теперь на конкретных примерах посмотрим, как все это можно применять.
Предположим, что вам надо создать какую-то тестирующую программу, в которой пользователю задается вопрос и он должен на него ответить за установленный интервал времени. Тогда желательно, чтобы, пока он что-то делает в приложении, работая над вопросом, ему постоянно высвечивалось бы оставшееся до окончательного ответа время. Попробуем промоделировать подобную задачу. Начните новый проект и разместите на форме метку и две кнопки. Метка (Label1) служит для отображения оставшегося времени. Кнопка "Пуск" (BBegin) имитирует задание вопроса. Кнопка "Ответ" (BResp) имитирует ответ пользователя.
Выполните команду "File/New/Other" и в открывшемся окне репозитария на странице "New" выберите пиктограмму "Thread Object". На вопрос об имени (Class Name) создаваемого класса, ответьте, например, MyTime.
В окне Редактора Кода вы увидите файл реализации модуля, который будет обеспечивать поток, связанный с отображением времени. Откройте заголовочный файл этого модуля с описанием введенного вами класса MyTime. В раздел public класса введите объявление двух переменных:
TDateTime T0, ТМах;
Переменная T0 будет хранить момент времени, в который задан вопрос, а переменная ТМах будет содержать заданный для ответа интервал времени. Обе переменные имеют тип TDateTime, используемый в C++Builder для хранения времени и дат. Значение ТМах можно задать в конструкторе введенного вами класса MyTime:
__fastcall MyTime::MyTime(bool CreateSuspended) : TThread(CreateSuspended) { TMax = EncodeTime (0, 0, 10, 0); }
Единственный оператор этого конструктора формирует значение ТМах с помощью функции EncodeTime, задавая интервал времени равным 10 секунд (третий параметр функции).
Сохраните проект, чтобы можно было без проблем ввести в коды ссылки модулей друг на друга. Назовите при сохранении первый (главный) модуль - UTime1, а второй - UTime2.
Формируемый нами поток должен будет отображать в метке Label1 главной формы Form1 время, оставшееся до окончания ответа. Поэтому прежде всего надо ввести ссылку в модуле UTime2 на модуль UTime1. Делается это или вставкой вручную в файл UTime2 директивы препроцессора
#include "UTime1.h"
или командой "File/Include Unit Hdr".
Аналогично надо сослаться из файла UTime1 на файл UTime2, поскольку из главной формы надо будет запускать и останавливать выполнение потока.
Так как поток будет работать с меткой Label1, являющейся компонентом VCL, надо, как указывалось в предыдущем разделе, использовать метод Synchronize и оформить операторы, работающие с меткой, в виде отдельной функции. Введите в раздел private описания класса потока объявление этой функции NewCaption:
void __fastcall NewCaption();
Реализация функции NewCaption может состоять всего из одного оператора:
void __fastcall MyTime::MewCaption() { Form1->Label1->Caption = TimeToStr (ТМах - (Now () - T0)); )
Аналогично надо оформить в виде отдельной функции отображение результатов тестирования. Для этого введите в раздел private описания класса потока объявление еще одной функции - Result:
void __fastcall Result ();
Ее реализация может быть следующей:
void __fastcall MyTime::Result () { if (Terminated) ShowMessage ("Ответ получен своевременно"); else ShowMessage ("Время на ответ исчерпано"); }
Тогда реализация функции Execute может выглядеть следующим образом:
void __fastcall MyTime::Execute () { Т0 = Now (); do { Synchronize (NewCaption); } while (!Terminated && ((double) (TMax - (Now () - T0}) >= 0)); Synchronize(Result); }
Этот код сначала запоминает в Т0 текущий момент времени, определяемый функцией Now(), Затем выполняется цикл do ... while до тех пор, пока или выполнение не будет прервано ответом пользователя (при ответе, как увидим позднее, значение свойства Terminated установится в true), или не истечет отведенное на ответ время (разность Now() - T0 станет больше или равной заданному значению ТМах). В тело цикла помещен вызов с помощью Synchronize функции Result, которая выдает сообщение, фиксирующее получение ответа вовремя или окончание отведенного на ответ времени.
Теперь вернитесь к основному модулю. Введите в файл его реализации глобальную переменную:
MyTime *Thread;
Эта переменная - объект нити Thread созданного вами класса MyTime. В обработчик щелчка на кнопке BBegin (начало отсчета времени) вставьте код:
Thread = new MyTime (false); Thread->FreeOnTerminate = true;
Первый оператор этого кода создает объект нити, передавая в него параметр false, что означает немедленное начало выполнения нити. Третий оператор задает значение свойства FreeOnTcrrainate, обеспечивающее разрушение объекта нити после окончания ее выполнения.
В обработчик щелчка на кнопке BResp (ответ пользователя) вставьте оператор:
Thread->Terminate ();
Программирование закончено. Приведем в заключение, чтобы не возникло какой-то путаницы, основные коды файлов, исключая стандартные директивы компилятора:
// файл UTime2.h: class MyTime : public TThread { private: void __fastcall HewCaption (); void __fastcall Result (); protected: void __fastcall Execute (); public: __fastcall MyTime (bool CreateSuspended); TDateTime T0, TMax; }; // файл UTime2.cpp: #include "UTime1.h" __fastcall MyTime::MyTime (bool CreateSuspended) : TThread (CreateSuspended) { TMax = EncodeTime (0, 0, 10, 0); } void __fastcall MyTime::HewCaption () { Form1->Label1->Caption = TimeToStr (TMax - (Now () - T0)); } void __fastcall MyTime::Result () { if (Terminated) ShowMessage ("Ответ получен своевременно"); else ShowMessage ("Время на ответ исчерпано"); } void __fastcall MyTime::Execute () { T0 = Now (); do { Synchronize (NewCaption); } while (!Terminated && ((double)(TMax - (Now () - T0)) >= 0)); Synchronize (Result); } // файл UTime1.cpp #include "UTime2.h" ... MyTime *Thread; ... void __fastcall TForm1::BBeginClick (TObject *Sender) { Thread = new MyTime (false); Thread->FreeOnTerminate = true; } void __fastcall TForm1::BRespClick (TObject *Sender) { Thread->Terminate (); }
Можете компилировать приложение, выполнять его и тестировать.
Теперь давайте усложним приложение, чтобы в нем было две нити, причем одна ждала бы другую. Введем нить, которая подводит итоги тестирования и в зависимости от полученного результат отображает сообщение, прошел или не прошел пользователь тестирование. Измените в модуле UTime2 функцию Result следующим образом:
void __fastcall MyTime::Result () { if (Terminated) { ShowMessage ("Ответ получен своевременно"); ReturnValue = 0; } else { ShowMessage ("Время на ответ исчерпано"); ReturnValue = 1; } }
В этой функции код возврата - свойство ReturnValue задается равным нулю в случае досрочного ответа и равным 1 в противном случае.
Выполните опять команду "File New Other" и в окне репозитария на странице New выберите опять пиктограмму "Thread Object". На вопрос об имени создаваемого класса ответьте "TResult". Тем самым вы создали в приложении еще один класс нити. Сохраните модуль под именем Time3.
Введите в разделе private объявления класса этого модуля функцию Result2 и целую переменную Res:
class TResult : public TThread { private: void __fastcall TResult::Result2(); int Res; protected: void __fastcall Execute(); public: __fastcall TResult (bool CreateSuspended); };
Переменная Res будет хранить код возврата потока Thread, а реализация функции Result2, обеспечивающей отображение результатов тестирования, может выглядеть следующим образом:
void __fastcall TResult::Result2 () { if (Res == 0) ShowMessage ("Вы успешно прошли тестирование"); else ShowMessage ("Вы не прошли тестирование"); }
Функцию Execute класса TResult запишите так:
void __fastcall TResult::Execute () { Res = Thread->WaitFor (); Synchronize (Result2); delete Thread; }
Первый оператор функции обеспечивает методом WaitFor ожидание окончания нити Thread и заносит ее код возврата в переменную Res. Функция Result2, вызываемая с помощью Synchronize, анализирует значение Res и выдает соответствующее сообщение. Чтобы можно было анализировать код возврата Thread, объект этой нити не должен разрушаться при завершении выполнения. Поэтому его свойство FreeOnTerminate должно быть равно false (это значение оно имеет по умолчанию). Но тогда надо принять меры к явному разрушению объекта. Поэтому в приведенный выше код добавлен оператор delete Thread.
Чтобы в модуле Time3 в приведенных операторах можно было обращаться к объекту Thread, объявленному в модуле Time1, надо повторить в Time3 объявление Thread, снабдив его спецификацией extern:
extern MyTime *Thread;
А поскольку в этом объявлении используется класс MyTime, объявленный в заголовочном файле UTime2.h, надо ввести директиву, ссылающуюся на этот модуль:
#include "UTime2.h"
Это все, что надо сделать в модуле Time3. А в модуль Time1 надо внести ссылку на Time3:
#include "UTime3.h"
объявить переменную типа TResult:
TResult *Result;
и изменить обработчик щелчка на кнопке BBegin:
Thread = new MyTime (false}; Result = new TResult (false); Result->FreeOnTerminate = true;
В этом коде в отличие от предыдущего примера для объекта Thread оставляется значение по умолчанию свойства FreeOnTermmate (false), создается объект второго потока Result и устанавливается в true его свойство FreeOnTerminate.
Вы можете скомпилировать приложение, выполнить его и убедиться, что обе нити работают, причем вторая ждет завершения первой и анализирует ее код возврата.
Теперь давайте построим более сложный пример с двумя нитями. В котором, в частности, можно будет увидеть влияние приоритетов на выполнение нитей. К каждой нити относится свой таймер (Timer1 и Timer2), своя метка (Label1 и Label2), в которую заносится число срабатываний таймера, свои кнопки, которые осуществляют соответственно пуск нити (Пуск — кнопки BRes1 и BRes2) после паузы, вызываемой нажатием кнопок Пауза (BSuspend1 и BSuspend2), и завершение выполнения (Останов — кнопки BTerm1 и BTerm2), вызывающее разрушение объекта нити. Кнопка Синхронно (BSync) сбрасывает счетчики обеих нитей и запускает их одновременно. При этом можно наблюдать различие в скорости работы, вызываемое различными приоритетами, выбираемыми пользователем в выпадающих списках ComboBox.
Так как обе нити функционируют одинаково, в данном случае достаточно описать только один класс нити. Выполните обычные процедуры по созданию модуля этого класса потока. Основные фрагменты его кода приведены ниже:
// файл Unit2.h: class T1 : public TThread { private: void __fastcall UpdateTimer(); protected: void __fastcall Execute(); public: ___fastcall T1 (bool CreateSuspended); TTimer *Timer; }; // файл Unit2.срр: void __fastcall T1::Execute() { do { Synchronize(UpdateTimer); } while (!Terminated); } void __fastcall T1::UpdateTimer() { Timer->Enabled = !Terminated; }
В этом коде в класс вводится переменная Timer - таймер. Процедура Execute, вызывая процедуру UpdateTimer, задает свойство таймера Enabled равным true, пока нить выполняется, и равным false в момент окончания выполнения. Таким образом, таймер запускается при выполнении нити и останавливается при ее завершении.
Теперь рассмотрим главную форму. На ней у таймеров Timer1 и Timer2 в процессе проектирования задайте Enabled = false. В списки ComboBox занесите строки
tpIdle tpLowest tpLower tpNormal tpHigher tpHighest tpTimeCritical
описывающие возможные значения приоритетов потоков.
Сошлитесь на модуль Unit2.h:
#include "UTime2.h"
и введите глобальные переменные:
int i1 = 0, i2 = 0; T1 *Thread1, *Thread2; bool Term1, Term2;
Переменные i1 и i2 - счетчики, по которым можно судить о скорости выполнения нитей. Переменные Thread1 и Thread2 - объекты потоков. А переменные Term1 и Term2 будут показывать, завершен ли соответствующий поток.
Ниже приведен код функций главной формы:
void __fastcall TForm1::FormCreate(TObject *Sender) { ComboBox1->ItemIndex = 3; ComboBox2->ItemIndex = 3; Thread1 = new T1 (false); // Запускается сразу Thread1->Timer = Timer1; Thread1->FreeOnTerminate = true; Term1 = false; Thread2 = new T1 (true); // Сразу не запускается Thread2->Timer = Timer2; Thread2->FreeOnTerminate = true; Term2 = false; } void __fastcall TForm1::Timer1Timer (TObject *Sender) { i1++; Label1->Caption = i1; Timer1->Enabled = false; } void __fastcall TForm1::Timer2Timer (TObject *Sender) { i2++; Label2->Caption = i2; Timer2->Enabled = false; } void __fastcall TForm1::BRes1Click (TObject *Sender) { if (!Term1) Thread1->Resume (); else ShowMessage ("Поток уже завершен"); ) void __fastcall TForm1::BRes2Click (TObject *Sender) { if (!Term2) Thread2->Resume (); else ShowMessage ("Поток уже завершен"); } void __fastcall TForm1::BTerm1Click (TObject *Sender) { Thread1->Terminate (); Term1 = true; } void __fastcall TForm1::BTerm2Click (TObject *Sender) { Thread2->Terminate (); Term2 = true; } void __fastcall TForm1::BSuspend1Click (TObject *Sender) { if (!Term1) Thread1->Suspend (); else ShowMessage ("Поток уже завершен"); } void __fastcall TForm1::BSuspend2Click (TObject *Sender) { if (!Term2) Thread2->Suspend (); else ShowMessage ("Поток уже завершен"); } void __fastcall TForm1::BSyncClick (TObject *Sender) { if (Term1 || Term2) { ShowMessage ("Поток уже завершен"); } else { if (!Thread1->Suspended) Thread1->Suspend(); if (!Thread2->Suspended) Thread2->Suspend(); Timer1->Enabled = false; Timer2->Enabled = false; i1 = 0; i2 = 0; Thread1->Resume (); Thread2->Resume (); } } void __fastcall TForm1::ComboBox1Change (TObject *Sender) { if (Term1) { ShowMessage ("Поток уже завершен"); } else { switch (ComboBox1->ItemIndex) { case 0: Thread1->Priority = tpIdle; break; case 1: Thread1->Priority = tpLowest; break; case 2: Thread1->Priority = tpLower; break; case 3: Thread1->Priority = tpNormal; break; case 4: Thread1->Priority = tpHigher; break; case 5: Thread1->Priority = tpHighest; break; case 6: Thread1->Priority = tpTimeCritical; break; } } } void __fastcall TForm1::ComboBox2Change (TObject *Sender) { if (Term2) { ShowMessage ("Поток уже завершен"); } else { switch (ComboBox2->ItemIndex) { case 0: Thread2->Priority = tpIdle; break; case 1: Thread2->Priority = tpLowest; break; case 2: Thread2->Priority = tpLower; break; case 3: Thread2->Priority = tpNormal; break; case 4: Thread2->Priority = tpHigher; break; case 5: Thread2->Priority = tpHighest; break; case 6: Thread2->Priority = tpTimeCritical; break; } } }
Процедура FormCreate создает объекты нитей и присваивает их свойствам Timer соответствующие таймеры, расположенные на форме. Свойства нитей FreeOnTerminate задаются равными true, чтобы по завершении выполнения объекты нитей автоматически разрушались. В приведенном коде для разнообразия первая нить начинает выполняться сразу после запуска приложения после создания объекта нити в обработчике события OnCreate формы. Вторая нить создается с передачей в конструктор значения true, что означает, что нить начнет выполняться только после вызова метода Resume (вызывается при щелчке на кнопках Пуск — процедуры BRes1Click и BRes2Click). Щелчки на кнопках Пауза (процедуры BSuspend1Click и BSuspend2Click) приостанавливают выполнение соответствующей нити методами Suspend. Щелчки на кнопках Останов (процедуры BTerm1Click и BTerm2Click) завершают выполнение соответствующей нити. Щелчок на кнопке Синхронизация (процедура BSyncClick) приостанавливает выполнение обеих нитей (если они в данный момент не приостановлены) и таймеров, сбрасывает счетчики il и i2 на 0 и повторно запускает выполнение нитей. Изменение выбора в выпадающих списках (процедуры ComboBox1Change и ComboBox2Change) изменяет приоритеты нитей.
Для того чтобы после завершения того или иного потока не возникало ошибок при попытке выполнить какие-то его методы или задать свойства, в функциях введены проверки переменных Term1 и Term2, значения которых делаются равными true в процедурах BTerm1Click и BTerm2Click.
Сохраните проект, скомпилируйте его и проверьте в различных режимах. Изменяя приоритеты нитей, вы сможете увидеть, что одинаковые вначале показания счетчиков начинают с течением времени расходиться. Нить с приоритетом, меньшим нормального, работает медленнее. Вы можете также увидеть. что многократная приостановка выполнения нити методом Suspend требует такого же числа ее запуска методом Resume, прежде чем она действительно начнет выполняться.