Автоматизированное место врача от Leybasoft®
Главная » 2017 » Сентябрь » 6 » TThread: с чем его едят (часть 2)
1:12 AM
TThread: с чем его едят (часть 2)

Начало здесь

5. TThread: передача простых типов туда и обратно
6. TThread: использование "встроенных" методов Synchronize и Queue
7. TThread: передача бинарных данных туда и обратно
8. TThread: универсализм применения

 

5. TThread: передача простых типов туда и обратно.


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

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

примечание: о передаче бинарных данных (блобов, картинок и проч.) чуть дальше.

Тогда конструктор в объявлении наследника TThread будет выглядеть, например, так

{ TMyThread }
TMyThread = class(TThread)
private
 FStr1, FStr2: String;
 FInt1, FInt2: Integer;
protected
 procedure Execute; override;
public
 constructor Create (CreateSuspended: boolean; var AStr1, AStr2: String; var AInt1,AInt2: Integer);
end;


а сам конструктор так

constructor TMyThread.Create(CreateSuspended: boolean; var AStr1, AStr2: String; var AInt1,AInt2: Integer);
begin
<skiped>
 FStr1:= AStr1;
 FStr2:= AStr2;
 FInt1:= AInt1;
 FInt2:= AInt2;
<skiped>
end; 


Соответственно, с полями самого класса TMyThread можно работать в его методе Execute.

Существенным недостатком такого подхода является фиксированное количество параметров и обязательное их указание при создании экземпляра TMyThread, а также то, что "на каждый чих" придется создавать еще одного наследника TThread со "своим" набором параметров. Избежать этого можно, если в качестве параметра передавать динамический массив определенного типа, например

constructor Create (CreateSuspended: boolean; AStrArr: Array of String; AIntArr: Array of Integer {или AConst: Array of const});

Тогда в объявлении класса TMyThread достаточно объявить соответствующие приватные поля

TMyThread = class(TThread)
private
 //массивы-поля
 FStrArr: Array of String;
 FIntArr: Array of Integer;


и в конструкторе просто инициализировать их

constructor TMyThread.Create(CreateSuspended: boolean; AStrArr: Array of String; AIntArr: Array of Integer {или AConst: Array of const});
var i: Integer;
begin
 <skiped>
 if Length(AStrArr) > 0 then //если длина массива-параметра ненулевая
 begin
 SetLength(AStrArr,Length(AStrArr));//устанавливаем длину массива-поля

 for i := Low(AStrArr) to High(AStrArr) do //инициализируем поля массива-поля
 AStrArr[i]:= AStrArr[i];
 end;
//то же самое делаем для массива FIntArr
<skiped>
end; 


Здесь уже передается массив, длина которого может быть и нулевой (т.е. достаточно будет написать в параметрах Create(True,[],[1,2,3]). Таким образом достигается некоторая универсальность класса.
Но и у этого способа есть недостаток: если количество типов, передаваемых в конструктор , большое, то код становится громоздким и плохо читаемым. Выйти из положения можно, передавая параметром массив записи(структуры), которая может иметь сколько угодно полей и какого угодно типа. Например

type
TMyDataRec = packed record
 AInt: Integer;
 AStr: String;
 AVar: Variant;
end;

TMyDataRecArr: array of TMyDataRec;
 
{ TMyThread }
TMyThread = class(TThread)
 private
 FArrDataRec: array of TMyDataRec;
 protected
 procedure Execute; override;
 public
 constructor Create (CreateSuspended: boolean; ArrDataRec: array of TMyDataRec);
//или даже так
//constructor Create (CreateSuspended: boolean; ADataRecArr: TMyDataRecArr);
end;


в конструкторе

constructor TMyThread.Create(CreateSuspended: boolean; ArrDataRec: array of TMyDataRec);
var i: Integer;
begin
 <skiped>
 if Length(ArrDataRec) > 0 then
 begin
 SetLength(FArrDataRec,Length(ArrDataRec));

 for i := Low(ArrDataRec) to High(ArrDataRec) do
 begin
 FArrDataRec[i].AInt:= ArrDataRec[i].AInt;
 FArrDataRec[i].AStr:= ArrDataRec[i].AStr;
 FArrDataRec[i].AVar:= ArrDataRec[i].AVar;
 end;
 end;
 <skiped> 
end;


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

С передачей целых чисел проблем нет.

Примечание: ну, или почти нет. Исторически сложилось, что в API x32 изначально WParam - был тип Word (W[ord]Param), а LParam - LongInt (L[ongint])Param. После появления x64-разрядных приложений эти параметры были преобразованы соответственно в UINT_PTR и LONG_PTR (подробное обсуждение можно почитать здесь. Выводы из того  обсуждения таковы:
1) если данные передаются в пределах приложений одной разрядности, то неважно как их интерпретировать, как Int или как UInt соответствующей разрядности, т.к. они передаются "как есть"
2) если данные передаются в пределах приложений разной разрядности, то преобразование происходит так:
- из х64 в х86: просто отбрасываются 32 старших разряда;
- из х86 в х64: для WParam просто дорисовываются 32 нуля слева, что превращает WPARAM в UInt64, при этом получаются сюрпризы, если там предполагалось отрицательное число со знаком;
Для LPARAM - идёт расширение со знаком, т.е. значение интерпретируется как Int32, после этого расширяется до Int64 с сохранением знака. Если там предполагалось беззнаковое число больше MaxInt[32], то снова получаются сюрпризы.

Таком образом, если значения в параметрах передаются в пределах одного процесса или между приложениями одной разрядности, то формально оба параметра можно считать параметрами одного типа (в Lazarus WParam = LParam = тип DWord(x32) и Int64(x64)).


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

procedure TMyThread.Execute;
var
Msg: TLMessage;
begin
Msg.wParam:= -123;
//Msg.wParam:= NativeInt(-123);// можно так
Msg.lParam:= 456;                         
SendMessage(Form1.Handle,WM_INT_MSG,Msg.wParam,Msg.lParam);
...
end;

procedure TForm1.WM_Int_Msg(var Msg: TLMessage);
begin
//оба способа корректны
Label1.Caption:= IntToStr(NativeInt(Msg.wParam)) ;
Label2.Caption:= IntToStr(Msg.lParam) ;
end;

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

procedure TMyThread.Execute;
var
Msg: TLMessage;
s1,s2: String;
begin
s1:= 'Яблоко';
s2:= 'Apple';
Msg.wParam:= NativeInt(s1);//тип LongInt только для x32 {1}
Msg.lParam:= IntPtr(PChar(s2));{2} //еще один способ
SendMessage(Form1.Handle,WM_STR_MSG,Msg.wParam,Msg.lParam);
...
end;

procedure TForm1.WM_Str_Msg(var Msg: TLMessage);
begin
Label1.Caption:= String(Msg.wParam) ;{1}
Label2.SetTextBuf(PChar(Msg.lParam));{2}
end;
 

Update (13.04.2019):   для программирующих на FPC/Lazarus настоятельно рекомендуется вместо NativeInt/NativeUInt использовать соответственно IntPtr/UIntPtr. В справке к fpc это обосновывается так: "NativeInt и NativeUInt являются типами совместимости c Delphi. Хотя Delphi [также]  имеет [типы] IntPtr и UIntPtr, документация Delphi для NativeInt утверждает, что 'размер NativeInt эквивалентен размеру указателя для текущей платформы'. Из-за вводящих в заблуждение имен эти типы не должны использоваться RTL FPC. Обратите внимание, что на i8086 их размер  изменяется между 16-битными и 32-битными [значениями] в зависимости от модели памяти, так что они на самом деле там вообще не тип int."

Update (21.02.2018): еще один "кошерный" способ передачи строк (взятый отсюда), который не боится потери памяти при передаче указателя на него.  Этот способ можно безопасно использовать в PostMessage

procedure TMyThread.Execute;
var Msg: TLMessage;
 s1: String;
begin
s1:= 'Яблоко';
PostMessage(Form1.Handle,WM_STR_MSG,WParam(s1),Msg.lParam);
Pointer(s1):= nil;
...
end;

procedure TForm1.WM_Str_Msg(var Msg: TLMessage);
var s1: String; 
begin
s1:= '';
WPARAM(s1):= Msg.wParam;
Label1.Caption:= S1;
end;


Обратите внимание, что приведение к типу LongInt будет корректно работать только для x32 платформ, но на платформах x64_32 это "обрежет" 4 старших байта указателя на строку и приведет к некорректной передаче данных.

Ну и пересылка записи(структуры)

procedure TMyThread.Execute;
var Msg: TLMessage;
begin
SetLength(TestArrRec,5);//задаем размер
//заполняем массив
for i := Low(TestArrRec) to High(TestArrRec) do
begin
  TestArrRec[i].AInt:= i;
  TestArrRec[i].AStr:= 'Str ' + IntToStr(i);
  TestArrRec[i].AVar:= 'Var ' + IntToStr(i);
end;
SendMessage(Form1.Handle,WM_1_MSG,0,LPARAM(NativeInt(@FMyArr[2])));

... 
end;


procedure TForm1.WM_Some_Msg(var Msg: TLMessage);
var TestRec: TMyDataRec;
begin
TestRec:= TMyDataRec(Pointer(Msg.lParam)^);
Label1.Caption:= Format('AInt = %d, AStr = %s, AVar = %s',[TestRec.AInt, TestRec.AStr,VarToStr(TestRec.AVar)]);
//результат
'AInt = 2, AStr = Str 2, AVar = Var 2'
end;
 

6. TThread: использование "встроенных" методов Synchronize и Queue.


В контексте разговора о передаче данных из дополнительного потока в основной необходимо упомянуть о "встроенных" методах самого класса TThread. Это методы Synchronize (аналог SendMessage) и Queue (аналог PostMessage). Это полностью потокобезопасные методы, которые при правильном применении исключают утечку памяти  без дополнительных телодвижений. Хотя они не такие гибкие, как SendMessage/PostMessage. Особенность их применения в том, они являются процедурами без параметров. Поэтому в них можно использовать только переменные, объявленные как поля класса TThread. Например, объявим класс потока следующим образом
 

type

{ TMyThread }

TMyThread = class(TThread)
private
 FLblMsg,
 FBtnMsg: Integer;
 procedure SendMsg2Label;
 procedure SendMsg2Button;
protected
 procedure Execute; override;
public
 constructor Create(CreateSuspended: Boolean);
end;


Выше мы объявили переменные FLblMsg,  FBtnMsg: Integer, которые использоваться в любой процедуре, в том числе и в процедурах SendMsg2Label и SendMsg2Button. Чтобы проиллюстрировать их работу, создадим проект, где из потока будет передаваться в заголовок Label текущее значение инкрементального счетчика (метод SendMsg2Label), а в заголовок кнопки - оставшееся (метод SendMsg2Button).  Код в них реализуется так:
 

procedure TMyThread.SendMsg2Label;
begin
 FrmMain.Label1.Caption:= Format('Текущее значение счетчика: %d',[FLblMsg]);
end;

procedure TMyThread.SendMsg2Button;
begin
 FrmMain.Button1.Caption:= IntToStr(FBtnMsg);
end;


Используем эти методы в Execute доп. потока следующим образом:
 

procedure TMyThread.Execute;
const
 k: Integer = 100;
var
 i: Integer;
begin
 for i:= 0 to k do
 begin
 FLblMsg:= i;//присвоим текущее значение счетчика
 Queue(@SendMsg2Label);//аналог PostMessage

 FBtnMsg:= k - i;//присвоим оставшееся значение счетчика
 Synchronize(@SendMsg2Button);//аналог SendMessage
 Sleep(50);
 end;
end; 


Как видно из выше приведенного кода, в каждой итерации цикла сначала присваивается текущее значение счетчика переменной FLblMsg, затем вызывается метод SendMsg2Label (где из доп.потока изменяется содержимое визуального компонента Label основного потока), причем доп.поток не дожидается окончания работы метода SendMsg2Label (вот он, Queue - аналог PostMessage), а изменяет значение FBtnMsg и вызываем метод SendMsg2Button (где из доп.потока изменяется содержимое визуального компонента Button основного потока). 

Вот тут доп. поток уже ждет окончания работы метода SendMsg2Button (вот он, Synchronize - аналог SendMessage), после чего выполняет следующую процедуру Sleep(50). В результате получается примерно следующее



на windows



на Linux


Тестовый проект прилагается.


 

7. TThread: передача бинарных данных туда и обратно.


Данные большого размера внутрь доп.потока и наружу из него чаще приходится передавать при работе с базами данных (т.н. блобы). Их можно передавать с выделением памяти (как массив байтов) или без ее выделения (передавая указатель).  Для работы создадим уже знакомый тип record (можно создавать и массив из указанной структуры, как было описано в предыдущей части, только к полям придется обращаться по индексу[порядковому номеру] этой структуры в массиве)
 

1) передача с использованием массива байтов

 

type
 TStreamRec = packed record
 ContainerType: Integer;//тип блоба(большой текст или картинка) - скорее, формальный параметр
 ByteArray: array of Byte;//массив байтов
 end;


Передаем блоб, например картинку, в поток
 

procedure TForm1.BtnThreadClick(Sender: TObject);
var
  ms: TMemoryStream; //поток для Picture
  MyStreamRec: TStreamRec;
begin
if Assigned(FMyThread) then Exit;
ms:= TMemoryStream.Create;//для картинки
try
  ImgSrc.Picture.SaveToStream(ms);
  ms.Position:= 0;

  SetLength(MyStreamRec.ByteArray, ms.Size);//определяем размер массива байтов поля TStreamRec
  ms.Read(MyStreamRec.ByteArray[0],ms.Size);//пишем содержимое потока в массив поля записи
  MyStreamArr.ContainerType:= 1;//тип контейнера TImage

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



Сам поток

{ TMyThread }
TMyThread = class(TThread)
private
  FStreamRec: TStreamRec;
protected
  procedure Execute; override;
public
  constructor Create (CreateSuspended: boolean; AStreamRec: TStreamRec);
end



Копируем данные во внутренние структуры в конструкторе доп.потока
constructor TMyThread.Create(CreateSuspended: boolean; AStreamRec: TStreamRec);
begin
...
 FStreamRec.ContainerType:= AStreamRec.ContainerType;
 SetLength(FStreamRec.ByteArray,Length(AStreamRec.ByteArray));//определяем размер массива байтов
 FStreamRec.ByteArray:= AStreamRec.ByteArray;
...
end; 


Сохраняем данные внутри  доп. потока, например, в БД
procedure TMyThread.Execute;
var
 ms: TMemoryStream;
begin
 ms:= TMemoryStream.Create;
 try
 ms.Write(FStreamRec.ByteArray[0],Length(FStreamRec.ByteArray));//записываем в MemoryStream содержимое массива байтов
 ms.Position:= soFromBeginning;//сдвигаемся к началу потока
 TBlobField(Table1.FieldByName('Picture')).LoadFromStream(ms);//сохраняем картинку в БД
 finally
 FreeAndNil(ms);
 end;
end;


Передача блобов из доп.потока в основной поток
procedure TMyThread.Execute;
var
ms: TMemoryStream;
begin
 ms:= TMemoryStream.Create;
 try
 //загружаем в MemoryStream бинарные данные из файла или блоб-поля БД
 //копируем MemoryStream в массив байтов способом, аналогичным выше описанному
 ...
 //отсылаем "целочисленный" указатель структуры в основной поток
 SendMessage(Form1.Handle,WM_ADDSTREAMPARAM_MSG,0;LPARAM(@FStreamRec));
 finally
 FreeAndNil(ms);
 end;
end;

В основном потоке получаем данные
procedure TForm1.WM_AddStreamParam_Msg(var Msg: TLMessage);
var
ARec: TStreamRec;
ms: TMemoryStream;
begin
 ARec:= TStreamRec(Pointer(Msg.lParam)^);
 ms:= TMemoryStream.Create;
 try
 ms.Write(ARec.ByteArray[0],Length(ARec.ByteArray));
 ms.Position:= soFromBeginning;
 ImgDest.Picture.LoadFromStream(ms);
 finally
 FreeAndNil(ms);
 end;
end;

2) передача с использованием указателей


Введем новый тип PStreamPtr (указатель на MemoryStream) и модифицируем немного выше описанную структуру так

type
PStreamPtr = ^TMemoryStream

TStreamRec = packed record
ContainerType: Integer;//тип блоба(большой текст или картинка) - скорее, формальный параметр
StreamPtr: PStreamPtr;//указатель на MemoryStream
end;


Теперь загружаем и передаем бинарные данные в доп.поток так

procedure TForm1.BtnThreadClick(Sender: TObject);
var
  ms: TMemoryStream; //поток для Picture
  MyStreamRec: TStreamRec;
begin
if Assigned(FMyThread) then Exit;
ms:= TMemoryStream.Create;//для картинки
try
  ImgSrc.Picture.SaveToStream(ms);
  ms.Position:= 0;

  MyStreamArr.ContainerType:= 1;//тип контейнера TImage

  MyStreamArr.StreamPtr:= @ms;//записываем в поле указатель на MemoryStream

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


В конструкторе потока инициализируем приватные поля-переменные


constructor TMyThread.Create(CreateSuspended: boolean; AStreamRec: TStreamRec);
begin
...
FStreamRec.ContainerType:= AStreamRec.ContainerType;
FStreamRec.
StreamPtr:= AStreamRec.StreamPtr;
...
end;


Сохраняем бинарные данные внутри доп.потока, например, в БД

procedure TMyThread.Execute;
var
ms: TMemoryStream;
begin
  ms:= TMemoryStream.Create;
  try
  ms.Write(FStreamRec.
StreamPtr^,Length(FStreamRec.StreamPtr^));//записываем в MemoryStream содержимое массива байтов
  ms.Position:= soFromBeginning;//сдвигаемся к началу потока
  TBlobField(Table1.FieldByName('Picture')).LoadFromStream(ms);//сохраняем картинку в БД
  finally
  FreeAndNil(ms);
  end;
end;


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

procedure TMyThread.Execute;
var
ms: TMemoryStream;
begin
  ms:= TMemoryStream.Create;
  try
  //загружаем в MemoryStream бинарные данные из файла или блоб-поля БД
  //копируем MemoryStream в массив байтов способом, аналогичным выше описанному
  ...
  //отсылаем "целочисленный" указатель структуры в основной поток
 SendMessage(Form1.Handle,WM_ADDSTREAMPARAM_MSG,1;LPARAM(@FStreamRec.StreamPtr^));
  finally
  FreeAndNil(ms);
  end;
end;


В основном потоке разыменовываем указатель и пишем бинарные данные в контейнер
procedure TForm1.WM_AddStreamParam_Msg(var Msg: TLMessage);
begin
case Msg.wParam of
0: MemoDest.Lines.LoadFromStream(TStream(Msg.lParam));
1: ImgDest.Picture.LoadFromStream(TStream(Msg.lParam));
end;
end; 

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

8. TThread: универсализм применения.


И напоследок. Можно написать кучу наследников TThread со своим набором полей, параметров конструктора и реализацией методов. Но все это загромождает код и затрудняет его отладку. Для себя я выбрал такой способ сделать наследника TThread более-менее универсальным. Я объявляю еще один тип, который будет определять, какая часть кода внутри Execute будет выполняться, и передаю его в качестве параметра в конструктор доп.потока
type
//вариант сценария работы дополнительного потока
 TMyThreadTarget = (mttConnectDB, mttDisconnectDB, mttOpenDSet {и т.д.});

{ TMyThread }
TMyThread = class(TThread)
private
 FThreadTarget: TMyThreadTarget;//свойство, определяющее, какую часть кода в потоке задействовать
protected
 procedure Execute; override;
public
 constructor Create(CreateSuspended: Boolean; {здесь любой набор параметров;} AThreadTarget: TMyThreadTarget);
end;


constructor TMyThread.Create(CreateSuspended: boolean; AThreadTarget: TMyThreadTarget);
begin
...
FThreadTarget:= AThreadTarget;
...
end;  

В самом методе Execute при помощи конструкции case..of реализуется нужная  часть кода.
 
procedure TMyThread.Execute;
begin
case FThreadTarget of
 mttConnectDB: ...
 mttDisconnectDB: ...
 mttOpenDSet: ...
end;
end; 

Вот и все, что мне известно обо всем этом на данный момент :)

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