Вернемся к многопоточности

Ещё задолго до Нового года я обещал вернуться к рассказу о многопоточных приложениях, начатому в №44 за прошлый год. Хотя мы с вами обсудили множество различных аспектов создания многопоточных приложений, многое, тем не менее, осталось за кадром.


Краткое содержание предыдущих серий

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

В первой статье ("Немного о многопоточности") я начал рассказ о различных аспектах создания многопоточных программ и о приёмах, применяемых при их написании. Основные из этих приёмов - это введение в программу специальных объектов, именуемых семафорами и мьютексами, об их свойствах я сейчас подробно говорить не буду. Во второй статье ("Ещё немного о многопоточности") мы поговорили о некоторых аспектах создания многопоточных приложений с помощью Delphi, и там, если помните, были некоторые неточности - именно из-за них, в конечном счёте, я и решил продолжить развитие данной темы на страницах газеты. Ну а в третьей статье ("И ещё немного о многопоточности") мы говорили с вами о создании многопоточных приложений в Java.

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


Новая серия

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

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


Synchronize vs. SendMessage

Для начала рассмотрим альтернативный вариант доступа к компонентам пользовательского интерфейса. Напомню, что в Delphi (или, вернее, в библиотеке VCL - она ведь используется и в C++ Builder'е) для упрощения структуры программы работа с пользовательским интерфейсом возможна только из одного потока. Метод Synchronize, который мы с вами использовали в нашем коде, предназначен вовсе не для синхронизации (разработчикам Delphi, к сожалению, удалось обмануть и меня, и я ещё раз приношу извинения за ошибку в рассмотрении кода), а для вызова методов класса-потока из основного потока приложения. Конечно, разработчики библиотеки VCL очень сильно упростили себе жизнь тем, что установили ограничения для её компонентов, которые работают внутри многопоточной программы в однопоточном режиме. Однако, к сожалению, нам теперь приходится иметь дело именно с такой архитектурой.

Методу Sychronize, который, по задумке создателей VCL, является панацеей, позволяющей многопоточному приложению использовать пользовательский интерфейс столь же полноценно, сколь и однопоточному, недостаёт, пожалуй, лишь одного качества - впрочем, качества довольно существенного. Ему недостаёт удобства. Давайте посмотрим, как этот метод определяется в VCL: procedure Synchronize(Method: TThreadMethod). Если теперь посмотреть на дефиницию класса TThreadMethod, то мы с вами безо всякого труда сможем увидеть, что за ней скрывается обычный процедурный метод без параметров: TThreadMethod = procedure of object. Что мы можем вынести из этого всего? Думаю, выводы хоть и просты, но весьма полезны: мы можем использовать метод Synchronize только для вызова тех методов, которые, во-первых, не имеют никаких параметров, а во-вторых, не возвращают никакого результата (то есть, объявлены как procedure, а не как function). На самом деле, конечно, это ограничение, будучи довольно-таки жёстким, очень даже легко обходится. Мы всегда можем добавить в наш класс потока новый метод, соответствующий предъявляемым к нему требованиям, а уже в рамках этого метода вызывать любые другие, и имеющие параметры, и возвращающие результат любого угодного душе типа. И, конечно, сделать новый метод в классе, унаследованном от TThread, совсем даже и не сложно, однако иногда это не самое удачное решение. Например, если нужно просто вывести какую-либо надпись или изменить состояние компонента ProgressBar, гораздо удобнее и быстрее будет не писать отдельный метод, а воспользоваться тем, что предложили не создатели VCL, а создатели Windows.

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

Как это выглядит на практике? Это можно увидеть в листинге:

procedure SampleThread.Execute;
var
s: string;
begin
repeat
s := 'Seconds passed since program start: ' + IntToStr(Timer);
SendMessage(Form1.Handle, WM_SETTEXT, 0, Integer(PChar(s)));
//Synchronize(SetThreadInfo);
Sleep(1000);
Inc(Timer);
until Terminated;
end;

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

Функция SendMessage (вместо неё здесь с тем же успехом могла бы быть использована и функция PostMessage - о различии между ними можно прочитать в справке по Windows API, которая, кстати, поставляется и вместе с Delphi) имеет четыре параметра. Описывается она следующим образом (взято из той самой справки, поэтому здесь описание на Си): LRESULT SendMessage(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam). hWnd - это дескриптор окна, которому мы посылаем сообщение, и почти все визуальные компоненты Delphi имеют его. Извлечь его можно с помощью свойства Handle. Впрочем, стоит иметь в виду, что не каждый визуальный компонент имеет дескриптор - например, если вы попытаетесь найти Handle у компонента TLabel, то вас постигнет разочарование, и поэтому для тех случаев, когда вам нужна надпись с Handle, используйте компонент StaticText. Второй параметр у функции SendMessage - это тип сообщения, которое мы посылаем. Здесь нужно использовать специальные именованные константы, которые можно найти в модуле Messages, который, кстати, нужно не забывать добавлять при этом в секцию uses того модуля, в котором содержится класс вашего отдельного потока. Какие сообщения нужно рассылать в каких ситуациях - это тоже лучше почитать при необходимости в справке по Windows API. Последние два параметра в SendMessage - это уже, собственно, параметры того сообщения, которое мы отсылаем нужному окну. Они будут зависеть от того, какое именно сообщение вы передаёте и что им хотите сказать, а потому какие-то общие рекомендации дать, к сожалению, невозможно.

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


Время работы потока

В конце этой статьи хочу остановиться на ещё одном интересном моменте - на определении времени работы того или иного потока. К сожалению, поскольку в многопоточном окружении работает не только один поток, то и таймер, выполняющий отсчёт в рамках одного потока, может давать существенную погрешность, связанную с тем, что наш поток приостанавливается системой безо всякого предупреждения, и точно так же снова запускается на выполнение. Тем не менее, поскольку иногда знать время, которое занимает какой-то один из потоков, всё-таки нужно, в Windows (только в линейке NT - NT 4, 2000, XP, 2003, Vista, 2008) предусмотрена специальная функция GetThreadTimes.

Выглядит описание этой функции, в общем-то, несколько даже, я бы сказал, угрожающе: BOOL GetThreadTimes(HANDLE hThread, PFILETIME lpCreationTime, LPFILETIME lpExitTime, LPFILETIME lpKernelTime, LPFILETIME lpUserTime). Однако на самом деле всё не так уж и сложно. Первый параметр - дескриптор того потока, для которого мы запрашиваем информацию о времени, второй и третий - это момент времени создания потока и выхода из него (в силу вполне понятных причин третий параметр, будучи, как и все остальные параметры типа LPFILETIME, указателем на структуру FILETIME, может указывать никуда). Последние два параметра - это количество времени потока, занятое на выполнение кода ОС, и количество времени, затраченное на выполнение кода пользовательского приложения.

Что касается типа FILETIME (который в Delphi назвали _FILETIME), то он выглядит так: _FILETIME = record dwLowDateTime: DWORD; dwHighDateTime: DWORD; end. Фактически же это 64-разрядное число, равное числу 100-наносекундных интервалов, прошедших с 01.01.1601. Для того, чтобы воспользоваться этим типом, можно его перевести в более понятный обычному человеку тип TSystemTime с помощью функции FileTimeToSystemTime. Есть и обратная функция - SystemTimeToFileTime. Поскольку структура TSystemTime довольно проста, то, думаю, приводить её тут нет смысла - чтобы разобраться с ней, хватит школьных знаний по английскому языку.

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


Итоги

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

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

Вадим СТАНКЕВИЧ

Версия для печатиВерсия для печати

Номер: 

01 за 2009 год

Рубрика: 

Software
Заметили ошибку? Выделите ее мышкой и нажмите Ctrl+Enter!