Автоматизированное место врача от Leybasoft®
Главная » 2017 » Август » 29 » TThread: с чем его едят (часть 1)
12:01 PM
TThread: с чем его едят (часть 1)
Про класс TThread вроде бы написано много и подробно. Но со временем некоторые нюансы забываются. Для себя сделал несколько заметок.

Не буду описывать все свойства класса TThread из справки, работу с критическими секциями, семафорами, глобальными переменными ThreadVar и проч. Будем считать, что читающий этот текст знает мат.часть (еще ссылки: тынц) Также не претендую на истину в последней инстанции. Замечания и критические поправки приветствуются.

1. TThread: конструктор
2. TThread: количество экземпляров
3. TThread: признак окончания работы
4. TThread: анимашки
 

Поскольку Лазарус - кроссплатформенная среда, то для корректной работы доп.потока под Линукс в модуль *.dpr проекта необходимо добавить менеджер работы с потоками POSIX (cthreads) и менеждер работы с памятью библиотеки C (cmem).  
program my_proj;
{$mode objfpc}
{$H+}
uses
 {$IFDEF UNIX}
 cthreads, cmem,
 {$ENDIF}
 Interfaces, // this includes the LCL widgetset
 Forms, main_u, splash_u;
{$R *.res} 

 
Далее, для работы с сообщениями, чтобы проект компилировался без ошибок, в разделе interface в uses необходимо к уже имеющимся дописать следующие модули

 

interface
uses
 ...
 , LCLType, LCLIntf ,LMessages; 
 

1. TThread: конструктор.

У доп.потока есть полезный параметр CreateSuspended: Boolean (по дефолту False, т.е. поток запускается сразу после создания). Иногда нужно приостановить работу потока, пока устанавливаются нужные его свойства или создается и активируется к показу окно сплэша (об этом ниже).

Итак, создаем потомка TThread с переопределенным конструктором

  { TMyThread }
 TMyThread = class(TThread)
 private
 public
 constructor Create (CreateSuspended: boolean);
 end;


В самом конструкторе напишем следующее:
 

constructor TMyThread.Create(CreateSuspended: boolean);
begin
 inherited Create(CreateSuspended);//создаем поток, используя всю мощь Силы методы предка
 Priority:= tpLower;//определяем приоритет потока
 FreeOnTerminate:= True;//указываем, будет ли он освобождать память самостоятельно после окончания своей работы или об этом должен заботиться программист
end; 


Тогда создание доп.потока из основного будет выглядеть так:

procedure TForm1.BtnThreadClick(Sender: TObject);
var
 FMyThread: TMyThread;
begin
 //создаем приостановленный поток
 FMyThread:= TMyThread.Create(True);
 ...
 //здесь какие-нибудь дополнительные телодвижения
 FMyThread.Start;//(или Resume) - теперь запускаем поток
end;
 

2. TThread: количество экземпляров.

В предыдущем примере экземпляр созданного нами объявлялся и создавался локально, в процедуре. При этом не приходится заботиться о его уничтожении, поскольку в конструкторе было объявлено FreeOnTerminate = True. Такой подход оправдан, если один или несколько потоков должны выполнять какую-то однотипную работу (очень хорошая статья по этому поводу есть на Королевстве Дельфи/оригинал автора ). Например:



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

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

 

{ TForm1 }
 TForm1 = class(TForm)
 <skiped>
 private
  FMyThread: TMyThread;
 public
  <skiped>
 end;  


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

procedure TForm1.BtnThreadClick(Sender: TObject);
begin
 if Assigned(FMyThread) then Exit;


Но есть один неочевидный нюанс. Свойство FreeOnTerminate = True освобождает память, занимаемую экземпляром TMyThread, но не об'nil'яет указатель на нее. Таким образом, выражение if Assigned(FMyThread) всегда будет True, и мы не сможем создать и запустить новый экземпляр TMyThread, даже если предыдущий отработал и испустил дух.

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

constructor TMyThread.Create(CreateSuspended: boolean);
begin
  inherited Create(CreateSuspended);
  FreeOnTerminate:= False;


Создание доп.потока при этом будет выглядеть так

procedure TForm1.BtnThreadClick(Sender: TObject);
begin
 //если предыдущий экземпляр доп.потока еще жив, то выйдем
 if Assigned(FMyThread) then Exit;

  //создаем доп.поток и ждем результата его работы
  FMyThread:= TMyThread.Create(True);
  try
  //тут что-то отображаем на форме или сплэше основного потока, пока доп.поток не завершит свою работу
  finally
 FreeAndNil(FMyThread);
  end; 
end;

3. TThread: признак окончания работы.

 

Чтобы основной поток знал, что доп.поток завершил свою работу,  у последнего существует public свойство TThread.Finished: boolean (свойство TThread.Terminated объявлено в секции protected и недоступно вне методов класса). Поскольку свойство TThread.Finished только для чтения, то практически оно применимо в ограниченном числе случаев, например, когда нужно пассивно дождаться окончания работы доп.потока. Если вы манипулируете временем жизни доп.потока, иногда полезно бывает самому контроллировать этот признак (например, если  надо дать основному потоку сигнал, что доп.поток окончен до его фактического окончания). Для этого введем флаг-поле в основном потоке

{ TForm1 }
TForm1 = class(TForm)
private  
public
{ public declarations }
FIsFinishedThread: Boolean; //признак окончания дополнительного потока
end; 
 


Тогда инициализацию свойства необходимо провести в конструкторе
 

constructor TMyThread.Create(CreateSuspended: boolean);
begin
 inherited Create(CreateSuspended);
 Form1.FIsFinishedThread:= False; //можно сделать и так, но лучше через SendMessage (см.ниже)


а по окончании работы доп.потока указать (выставить флаг), что поток завершен
 

procedure TMyThread.Execute;
begin
 //тут доп. поток выполняет основную работу

 Form1.FIsFinishedThread:= True;//можно сделать и так, но лучше через SendMessage (см.ниже)
//здесь выполняем еще какие-то действия до фактического завершения работы доп.потока
end; 


Тогда создание доп.потока будет выглядеть следующим образом


procedure TForm1.BtnThreadClick(Sender: TObject);
begin
 if Assigned(FMyThread) then Exit;

 //создаем поток с параметрами и ждем результаты
 FMyThread:= TMyThread.Create(True);
 try
 while not FIsFinishedThread do //пока доп.поток жив
 begin
 //просто ожидаем окончания работы доп. потока, на давая "заморозится" основному потоку
 Application.ProcessMessages;
 Sleep(500);
 end;
 finally
 FreeAndNil(FMyThread);
 end;
end; 


Цикл каких-то телодвижений (while..do) в главном потоке будет продолжаться до тех пор, пока флаг FIsFinishedThread не сменит значение с False на True, что позволит перейти к секции finally и уничтожить доп.поток.
 

4. TThread: анимашки.


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

Для начала необходимо создать сплэш окно в дизайнере среды, положив на форму контейнер для текста (TLabel, TStaticText или TMemo) и для картинки (TImage). Для циклической смены картинок и перерисовки элементов окна оптимальнее добавить TTimer с интервалом не более 500 мс (чисто эмпирическим путем я выяснил, что интервал менее 200 мс на Линуксе вызывает лаги при отрисовке сплэш-формы).

Если предполагается интерактивное взаимодействие с пользователем, то можно добавить несколько кнопок (поскольку кнопки OK и Cancel в винде и линуксе традиционно располагаются относительно друг друга с разных сторон, то названия кнопок и реакцию на их нажатие я формирую в коде, в зависимости от платформы).



Картинки можно хранить в ресурсах проекта, но мне показалось удобнее в TImageList. В качестве источника картинки лучше взять какой-нибудь gif-файл,  разобрать его на "запчасти" и загрузить в контейнер в виде последовательности картинок



Еще один нюанс: сплэш-форму лучше убрать из списка автосоздаваемых (auto-create forms),



создавая и уничтожая ее динамически. Это позволит экономить ресурсы машины.

Дальше кодим. Создадим переменные для LblMsg, BtnLeft и BtnRight соответственно, в которые будут передаваться строки из доп.потока.

type
 { TFrmSplash }
 TFrmSplash = class(TForm)
 private
 <skiped>
 public
 FBtnLeftCapt,
 FBtnRightCapt,
 FInMsgStr: String;
 end; 


Поскольку создавать и уничтожать форму мы будем динамически, то обязательно выставим TCloseAction так

procedure TFrmSplash.FormClose(Sender: TObject; var CloseAction: TCloseAction);
begin
 CloseAction:= caFree;
end; 


Теперь всем анимацию и отображение информации на форме описываем в событии таймера (не буду описывать все подробно, только уточню, что в первой половине кода к сообщению FInMsgStr в течение каждого интервала таймера прибавляется по одной точке в виде многоточия; во второй половине кода последовательно отображаются картинки из контейнера, имитируя ее анимацию)

procedure TFrmSplash.Timer1Timer(Sender: TObject);
begin
 case FDotCount of
 1: LblMsg.Caption := FInMsgStr;
 2: LblMsg.Caption := FInMsgStr + ' .';
 3: LblMsg.Caption := FInMsgStr + ' ..';
 4: LblMsg.Caption := FInMsgStr + ' ...';
 end;
 Inc(FDotCount);
 if FDotCount > 4 then FDotCount:= 1;

 //если список пуст, то будет отображаться статичная иконка приложения
 if ImgList.Count = 0
  then
 ImgSpl.Picture.Icon:= Application.Icon
  else
 ImgList.GetBitmap(FNumFrame, ImgSpl.Picture.Bitmap);

 Inc(FNumFrame);

 //начинаем с первой картинки, если добрались до последней
 if FNumFrame > pred(ImgList.Count) then FNumFrame := 0;
end; 


Теперь последний штрих

procedure TFrmSplash.FormCreate(Sender: TObject);
begin
 Position:= poOwnerFormCenter;
 LblMsg.Caption:= '';
 LblMsg.Transparent:= True;
 <skiped>
 FBtnLeftCapt:= BtnLeft.Caption;
 FBtnRightCapt:= BtnRigth.Caption;
 Timer1.Interval:= 500;
 Timer1.Enabled:= True;
end; 


и можно вызывать форму, запустив при этом доп.поток в уже знакомой нам процедуре

procedure TForm1.BtnThreadClick(Sender: TObject);
begin
 if Assigned(FMyThread) then Exit;//если поток все еще крутится, выйдем отседова

 //создаем приостановленный доп.поток
 FMyThread:= TMyThread.Create(True);
 try
  //создаем сплэш
 SplashForm:= TFrmSplash.Create(Application);

 //стартуем ожидающий доп.поток
 FMyThread.Start;
  try
 SplashForm.ShowModal;//показываем сплэш
  finally
 FreeAndNil(SplashForm);//уничтожаем сплэш 
  end;
 finally
 FreeAndNil(FMyThread);//уничтожаем доп.поток
 end;
end; 


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


constructor TMyThread.Create(CreateSuspended: boolean);
begin
  inherited Create(CreateSuspended);
  Priority:= tpLower;
  FreeOnTerminate:= True;
  <skiped>

  if CreateSuspended then Start;
end;


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

Вся работа доп.потока осуществляется в его методе Execute. "Общение" доп.потока с основным потоком лучше осуществлять посредством функций SendMessage и PostMessage через пользовательские сообщения. Об отличиях этих функций друг от друга можно почитать (например, здесь) в сети, и я не буду на этом останавливаться. Упомяну только, что ниже описанный способ с пользовательскими сообщениями работает только, если вы отправляете сообщения окнам одного процесса (своего приложения). Для межпроцессорного  (между различными приложениями) взаимодействия в качестве второго параметра необходимо использовать системные сообщения (WM_COPYDATA и др).

Поэтому сначала определим пользовательские сообщения

const
  {uses windows for WIN or LMessages for LINUX}
  WM_CLOSESPLASHFORM_MSG = WM_USER + $101;
  WM_GETFORSPLASHSTR_MSG = WM_USER + $102;
  WM_GETFORBTNLEFT_MSG = WM_USER + $103;
  WM_GETFORBTNRIGHT_MSG = WM_USER + $104;


и функции основного потока, где эти сообщения будут обрабатываться

{ TForm1 }
TForm1 = class(TForm)
//сообщение о закрытии сплэша
procedure WMCloseSplashFormMsg (var Msg: TLMessage); message WM_CLOSESPLASHFORM_MSG;
//сообщения из доп.потока для строки сообщений на сплэше
procedure WMGetForSplashStrMsg (var Msg: TLMessage); message WM_GETFORSPLASHSTR_MSG;
//сообщения из доп.потока для левой кнопки на сплэше
procedure WMGetForBtnLeftMsg (var Msg: TLMessage); message WM_GETFORBTNLEFT_MSG;
//сообщения из доп.потока для правой кнопки на сплэше
procedure WMGetForBtnRightMsg (var Msg: TLMessage); message WM_GETFORBTNRIGHT_MSG;
private
  { private declarations }
public
  { public declarations }
  FMsgRightBnt, //текст правой кнопки сплэша
  FMsgLeftBtn,  //текст левой кнопки сплэша
  FMsgSplashStr, //текст сообщения (LblMsg) сплэша
  FMsgSplashCapt: //текст заголовка окна (Caption) сплэша
              String
;
end;                                                                            


Не буду описывать содержимое всех функций, покажу только, как передать строки и закрыть сплэш-форму на частном примере коннекта к базе данных

 

procedure TMyThread.Execute;
var MyMsg: TLMessage;
begin
//коннектимся к базе (передаем сообщение в LblMsg на сплэше)
MyMsg.LParam:= NewString('Коннектимся к базе данных');{1}
SendMessage(Form1.Handle, WM_GETFORSPLASHSTR_MSG, 0, MyMsg.LParam);{2} 
<skiped>
//закроем сплэш
SendMessage(FrmMain.Handle,WM_CLOSESPLASHFORM_MSG,0,0);{3} 
end; 


Остановлюсь подробнее на том, что происходит в коде:
1) третий и четвертый параметр функции SendMessage - это целые числа, равные машинному слову (подробнее о параметрах WParam и LParam читайте чуть дальше). И, чтобы  передать строку, я воспользовался функцией NewString из замечательной библиотеки компонент для работы с потоками wthread (посмотрите исходники), которую написал и любезно предоставил в бесплатное пользование питерский программист wadman;

примечание: о способах передачи строк в сообщениях будет написано ниже.

2) в этой части кода, используя сообщение WM_GETFORSPLASHSTR_MSG, я пересылаю указатель на строку в процедуру WMGetForSplashStrMsg, где строка будет обработана и передана в сплэш (обратите внимание, здесь для перевода указателя на строку в саму строку используется обратная функция FreeString из все той же библиотеки wthread)


procedure TForm1.WMGetForSplashStrMsg(var Msg: TLMessage);
var s: String;
begin
 s:= FreeString(Msg.lParam);//переводим указатель обратно в строку
 if FMsgSplashStr <> s then //если предыдущая строка, переданная в эту процедуру, отличалась от этой
 begin
 FMsgSplashStr:= s;//присваиваем полю основного потока новое значение
 SplashForm.FInMsgStr:= FMsgSplashStr;//и пересылаем его в сплэш
 end;
end; 


Промежуточное поле-переменная FMsgSplashStr нужно для того, чтобы не пересылать одну и ту же строку в сплэш (обычный прием, распространенный VCL/LCL);

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

 

procedure TForm1.WMCloseSplashFormMsg(var Msg: TLMessage);
begin
 if Assigned(SplashForm) then SplashForm.Close;
end; 

 

В результате должно получиться нечто вроде этого



 

 

продолжение здесь

Категория: Программирование | Просмотров: 1584 | Добавил: leyba | Рейтинг: 0.0/0
Всего комментариев: 0
avatar