Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Теллес М. - Borland C++ Builder. Библиотека программиста - 1998

.pdf
Скачиваний:
790
Добавлен:
13.08.2013
Размер:
4.35 Mб
Скачать

Borland C++ Builder (+CD). Библиотека программиста 271

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

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

Итак, весь этот код приводит к вызову метода UpdatLabel, который собственно и делает всю работу в классе потока. Это весьма простой метод, как видно из листинга:

void __fastcall TCheckThread::UpdateLabel(void)

{

if ( pLabel ) pLabel->Caption = nCount; if ( nCount < 10000 ) nCount ++;

else nCount = 0;

}

Теперь, имея весь этот код класса потока, пора модифицировать класс формы: создавать поток, а затем работать с ним. Давайте обратим наше внимание на эту задачу.

Работа с потоками в форме

Чтобы работать с потоком, надо его создать. Это поручается форме, содержащей поток, и осуществляется простым вызовом оператора new для создания нового объекта «поток». В нашем примере мы создадим поток при создании формы. Создайте обработчик события формы OnCreate и поместите в метод FormCreate следующий код:

void __fastcall TForm1::FormCreate(TObject *Sender)

{

pThread = new TCheckThread(FALSE); pThread->AssignLabel( Label1 );

}

Когда вы создаете поток, то у вас есть возможность создать его либо приостановленным (suspended), либо нет. Параметр конструктора потока представляет собой булевское (логическое) значение, указывающее, в каком режиме создавать поток. Приостановленный режим это как будто вы «усыпляете» поток, пока вам не понадобится его «разбудить» из своего приложения. Если вы запустите поток в приостановленном режиме, то на вас ложится ответственность за вызов потока к жизни, выполняя метод Resume (продолжить) потока.

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

Borland C++ Builder (+CD). Библиотека программиста 272

такого кода выглядит так:

TMyThread::TMyThread( bool bSuspend )

: TThread (true) // приостановленный режим!

{

//Что-нибудь инициализировать

//Запустить поток, если нужно

if ( bSuspend == false ) Resume();

}

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

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

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

должен остановиться. Взглянув на метод Execute, мы видим, что он завершается, когда флаг Terminated равен true (истина). Для того чтобы обратить это значение (Terminated) в истинное, вам нужно вызвать метод класса потока Terminate (остановить). Добавьте следующий обработчик нажатия на кнопку Остановка в классе формы:

void __fastcall TForm1::Button1Click(TObject *Sender)

{

pThread->Terminate();

}

Еще один способ, каким можно (при)остановить и продолжить выполнение потока, — увидеть в действии, добавив в классе формы обработчик для кнопки Пауза:

void __fastcall TForm1::Button2Click(TObject *Sender)

{

pThread->Suspend();

}

Как видите, используется метод потока Suspend. Это временно остановит поток, но не убьет его. Метод Execute не вызывается, когда поток приостановлен. Есть одна странная деталь в отношении остановки потока методом Suspend. Вызов этого метода не является переключателем «выключить/включить» поток. Если вызвать Suspend несколько раз, то при каждом вызове будет нарастать счетчик. Чтобы по новой запустить поток, который останавливали несколько раз методом Suspend, вам придется вызвать метод Resume такое же число раз. Процесс выглядит

Borland C++ Builder (+CD). Библиотека программиста 273

примерно так:

Suspend : Счетчик = 1

Suspend : Счетчик = 2

Suspend : Счетчик = 3

Resume : Счетчик = 2 Поток не запускается

Resume : Счетчик = 1 Поток не запускается

Resume : Счетчик = 0 Поток наконец-то запускается

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

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

void __fastcall TForm1::Button3Click(TObject *Sender)

{

pThread->Resume();

}

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

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

Программа поиска в потоке

В нашем втором примере работы с потоками мы собираемся написать программу поиска, использующую потоки. Она позволит искать заданную строку в заданном каталоге. Также мы предоставим возможность выбрать маску файлов (например, все исходные файлы *.cpp), по которым будет происходить поиск. Когда будет нажата кнопка Начать поиск, форма запустит поток, который станет искать файлы в заданном каталоге, которые содержат нужную строку, и выводить имена файлов в окно списка, находящееся в главной форме.

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

Borland C++ Builder (+CD). Библиотека программиста 274

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

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

Рис. 13.2. Главная форма приложения «поиск в потоке»

Рис. 13.3. Форма приложения «поиск в потоке», отображающая файлы

Построение главной формы

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

Во-первых, надо разобраться с обработкой кнопки Начать поиск , которая будет начинать процесс поиска, запуская нужный поток. Создайте обработчик нажатия на кнопку Начать поиск и

Borland C++ Builder (+CD). Библиотека программиста 275

поместите в обработчик следующий код:

void __Fastcall TForm1::Button1Click(TObject *Sender)

{

pThread = new TSearchThread(Edit2->Text, Edit3->Text, Edit1->Text, FALSE);

SetOkToSearch( false );

}

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

void SetOkToSearch(bool bSearchOK)

{

Button1->Enabled = bSearchOk;

}

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

Разобравшись с этим, следующим делом надо разобраться с отображением текстового файла, который пользователь выбирает двойным щелчком мыши в окне списка. Создайте обработчик события DblClick (двойной щелчок) для объекта ListBox1 (окно списка) и добавьте в обработчик следующий код:

void __fastcall TForm1::ListBox1DblClick(TObject *Sender)

{

//Получить имя выбранного файла int nIdx = ListBox1->ItemIndex;

if ( nIdx != -1 )

{

AnsiString strFile = ListBox1->Items->Strings[nIdx];

//Создать новую форму для просмотра файла pFileViewer = new TForm3(this); pFileViewer->Memo1->Lines->LoadFromFile( strFile ); pFileViewer->Show();

}

}

В этом методе мы получаем имя файла, которое пользователь выбрал из окна списка. Список будет содержать имена файлов с указанием полного пути, так что функция может считать, что имя файла корректно (мы убедимся в этом позже) и просто использовать его для отображения файла. Само отображение целиком на совести объекта VCL TMemo, который сам знает, как напрямую загружать свое содержимое из файла. Используя метод LoadFromFile, мы загружаем весь выбранный файл прямо в память компонента TMemo, находящегося на вторичной форме.

Borland C++ Builder (+CD). Библиотека программиста 276

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

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

Создание потока для поиска

В данном примере поток создается точно так же, как и в предыдущем. Используй те систему создания объекта «поток», встроенную в CBuilder, для создания нового потокового класса с названием TSearchThread. CBuilder сгенерирует исходный файл (в данном примере Unit2.cpp), в котором будет определен класс.

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

__fastcall TSearchThread::TSearchThread(AnsiString strDir, AnsiString strMask, AnsiString

strText, bool CreateSuspended) : TThread (true)

{

FstrDirectory = strDir;

if ( FstrDirectory[FstrDirectory.Length()-1] != '\\' ) FstrDirectory += '\\';

FstrFileMask = strMask; FstrSearchText = strText; if ( !CreateSuspended )

Resume();

}

Вкупе с этими изменениями нам нужно немножко подправить и заголовочный файл для класса потока, чтобы он содержал описания переменных и методов класса TSearchThread. Вот изменения (выделенные подсветкой) в заголовочном файле Unit2.h для класса TSeachThread:

class TSearchThread : public TThread

{

private:

protected:

void __fastcall Execute(); private:

AnsiString FstrDirectory;

AnsiString FstrFileMask;

AnsiString FstrSearchText;

AnsiString FstrCurFileName;

int DoFind( AnsiString strSearchText, AnsiString& text ); public:

__fastcall TSearchThread(AnsiString strDir, AnsiString strMask, AnsiString strText, bool CreateSuspended); void __fastcall AddToListBox(void);

Borland C++ Builder (+CD). Библиотека программиста 277

};

Переменные-члены класса FstrDirectory, FstrFileMask и FstrSearchText используются для определения файлов, по которым производится поиск, а также текста, который нужно в них найти. Переменная, указывающая имя текущего файла (FstrCurFileName), нужна здесь, так как метод, через который происходит общение с объектом VCL на форме окном списка, — не имеет параметров. И наконец, в секцию private мы добавляем функцию DoFind, которая будет определять, содержит ли файл искомую строку.

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

void __fastcall TSearchThread::Execute()

{

//Проходим по всем файлам, которые подходят под маску

//Для этого используем функцию Win32 API

WIN32_FIND_DATA FindFileData;

AnsiString strSearchFiles = FstrDirectory + FstrFileMask; HANDLE hFirstFileHandle = FindFirstFile( strSearchFiles.c_str(), &FindFileData );

while ( hFirstFileHandle && !Terminated )

{

// Пытаемся открыть файл на чтение

FILE *fp = fopen(FindFileData.cFileName, "r" ); FstrCurFileName = FstrDirectory + FindFileData.cFileName; if (fp == NULL)

{

if ( FindNextFile( hFirstFileHandle, &FindFileData) == FALSE )

break;

continue;

}

// Ищем в данном файле построчно char szBuffer[ 256 ];

while ( !feof(fp) )

{

if ( fgets(szBuffer, 255, fp) == NULL ) break;

AnsiString s = szBuffer;

if ( DoFind( FstrSearchText, s ) )

{

Synchronize(AddToListBox);

break;

}

}

// Закрываем файл и переходим к следующему в списке fclose(fp);

if ( FindNextFile( hFirstFileHandle, &FindFileData ) == FALSE )

Borland C++ Builder (+CD). Библиотека программиста 278

break;

}

Form1->SearchComplete();

}

Этот код следует простому и прямолинейному процессу. Во-первых, маска файла и каталог используются для создания списка файлов для поиска, используя функции API FindFirstFile и FindNextFile, которые мы рассматривали выше, в главе, посвященной Windows API.

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

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

void __fastcall TForm1::SearchComplete(void)

{

pThread->Terminate(); SetOkToSearch( true );

}

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

//Метод осуществляет тупой поиск строки

//текста во входной строке

int TSearchThread::DoFind(AnsiString strSearchText, AnsiString& text)

{

int i=0;

int nCount = 0;

while ( i < text.Length() )

{

if ( !strncmp(text.c_str()+i, strSearchText.c_str(), strSearchText.Length()) )

{

i += strSearchText.Length(); nCount ++;

}

else

{

i++;

}

Borland C++ Builder (+CD). Библиотека программиста 279

}

return nCount;

}

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

void __fastcall TSearchThread::AddToListBox(void)

{

if ( FstrCurFileName.Length() ) Form1->ListBox1->Items->Add( FstrCurFileName );

}

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

Рис. 13.4. Программа фонового поиска в действии

На рис. 13.4 показан типичный поиск файлов на моем жестком диске, которому было сказано искать все исходные файлы (*.cpp), содержащие слово «Text». Результаты показаны в окне списка, и в то же время один из файлов показан в окне просмотра файлов. Поиск закончен, и кнопка Начать поиск снова доступна для запуска нового поиска.

Дизайн приложения с потоками

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

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

Borland C++ Builder (+CD). Библиотека программиста 280

часто можете просто «завернуть» код цикла обработки времени простоя в поток.

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

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

Преимуществ у потоков много. Вы можете сделать так, что каждая независимая часть кода будет совершать свою работу, совсем не беспокоясь о других частях. Например, код, управляющей визуальными элементами, может спокойно считать, что у него будут нужные данные в тот момент, когда они потребуются (так называемая система «just-in-time data retrieval», получение данных вовремя), вместо ожидания процесса загрузки данных. Потоки являются хорошим способом отделять код, отвечающий за получение данных, который сильно привязан к конкретному приложению, от кода графического интерфейса, управляющего отображением и манипулированием данными. В этом отношении потоки хорошо вписываются в объектно- ориентированный подход.

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

Перед тем как использовать потоки, задайте себе несколько вопросов:

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

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

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

Соседние файлы в предмете Программирование на C++