基于 QWorker 的多线程编程 – 传递参数给作业

在上一节中,我们看到这个示例实在是太简单了,实际在作业处理过程中,稍微复杂一点的作业,我们都需要传递参数给作业的处理函数。那么怎么传递参数给作业呢?

在 Post 函数的声明中,我们看到有一个 AData 参数,是一个无类型的指针,我们实际上就是通过它来作业传递参数。我们知道,无类型指针实际上是一个整数来记录地址的值,在32位程序中,它是32位的,在64位程序中,它是64位的,那么,我们实际上,对于小于等于32的内容,是可以直接传值的,这样做的好处是不需要额外的释放步骤,而64位的内容则需要视我们的具体程序环境而定,具体可参考下表:

L2Directs

当然,这些可以直接传值的类型,我们也需要做一些简单的技巧性处理,如传递一个字符时,Delphi 中就可能需要用到 Ord 函数传它的内码,而传递一个整数类型的值时,我们就需要强制转换一个,Pointer(值)即可,而传递浮点值时,则需要更复杂一点的转换,您可以用 absolute 语法,也可以使用取地址转换的办法来完成。如下所示:

//整数类型
Pointer(100);
//浮点类型-absolute
var
  f:Single;
  i:Integer absolute f;
begin
...
f:=1.2;
...
Pointer(i);
...
end;
//浮点类型-强转
Pointer(PInteger(@f)^);

当然,到了作业中相应的需要将Pointer反转换来就可以了,具体就不再缀述,省得大家认为我在骗字数。

上面的简单类型传递已经搞定了,那么复杂的类型传递我们怎么传呢?大家可能注意到了 Post 函数还有一个参数叫 AFreeType,它指定了 AData 指针关联的对象如何去释放。QWorker 默认提供了下面几种释放方式:

  • jdfFreeByUser :用户自己负责,QWorker 可以当甩手掌握的
  • jdfFreeAsObject:传递的是一个对象指针,QWorker 在作业生命周期结束时,调用对象的析构函数释放
  • jdfFreeAsSimpleRecord:传递的是一个简单记录类型,QWorker 在作业生命周期结束时,直接调用Dispose(Pointer) 来释放,注意不要传递包含字符串类型/接口/动态数组等类型的成员,否则会造成内存泄露
  • jdfFreeAsInterface:传递的是一个接口,QWorker 会自动调用 AddRef 和 Release 来维护其生命周期
  • jdfFreeAsC1 ~ jdfFreeAsC6:传递的是一个用户自定义的内存释放方式,由用户自行指定 Workers.OnCustomFreeData 事件的响应函数来处理

看起来挺多的,但实际的业务需要可能更复杂,那么我们能不能进一步简化呢?万能的神龙,变变变~~~TQJobExtData登场了,AData 参数传递的是TQJobExtData这个类型的一个实例,然后由它提供了一层封装。当然了,因为 TQJobExtData 是一个类,所以 AFreeType 必需手动指定为 jdfFreeAsObject。我们看这个类的声明:

TQJobExtData = class
  private
    function GetAsBoolean: Boolean;
    function GetAsDouble: Double;
    function GetAsInteger: Integer;
    function GetAsString: QStringW;
    procedure SetAsBoolean(const Value: Boolean);
    procedure SetAsDouble(const Value: Double);
    procedure SetAsInteger(const Value: Integer);
    procedure SetAsString(const Value: QStringW);
    function GetAsDateTime: TDateTime;
    procedure SetAsDateTime(const Value: TDateTime);
    function GetAsInt64: Int64;
    procedure SetAsInt64(const Value: Int64);
  protected
    FOrigin: Pointer;
    FOnFree: TQExtFreeEvent;
{$IFDEF UNICODE}
    FOnFreeA: TQExtFreeEventA;
{$ENDIF}
    procedure DoFreeAsString(AData: Pointer);
    procedure DoSimpleTypeFree(AData: Pointer);
{$IFNDEF NEXTGEN}
    function GetAsAnsiString: AnsiString;
    procedure SetAsAnsiString(const Value: AnsiString);
    procedure DoFreeAsAnsiString(AData: Pointer);
{$ENDIF}
  public
    constructor Create(AData: Pointer; AOnFree: TQExtFreeEvent); overload;
    constructor Create(AOnInit: TQExtInitEvent;
      AOnFree: TQExtFreeEvent); overload;
{$IFDEF UNICODE}
    constructor Create(AData: Pointer; AOnFree: TQExtFreeEventA); overload;
    constructor Create(AOnInit: TQExtInitEventA;
      AOnFree: TQExtFreeEventA); overload;
{$ENDIF}
    constructor Create(const Value: Int64); overload;
    constructor Create(const Value: Integer); overload;
    constructor Create(const Value: Boolean); overload;
    constructor Create(const Value: Double); overload;
    constructor CreateAsDateTime(const Value: TDateTime); overload;
    constructor Create(const S: QStringW); overload;
{$IFNDEF NEXTGEN}
    constructor Create(const S: AnsiString); overload;
{$ENDIF}
    destructor Destroy; override;
    property Origin: Pointer read FOrigin;
    property AsString: QStringW read GetAsString write SetAsString;
{$IFNDEF NEXTGEN}
    property AsAnsiString: AnsiString read GetAsAnsiString
      write SetAsAnsiString;
{$ENDIF}
    property AsInteger: Integer read GetAsInteger write SetAsInteger;
    property AsInt64: Int64 read GetAsInt64 write SetAsInt64;
    property AsFloat: Double read GetAsDouble write SetAsDouble;
    property AsBoolean: Boolean read GetAsBoolean write SetAsBoolean;
    property AsDateTime: TDateTime read GetAsDateTime write SetAsDateTime;
  end;

它在这里提供了常用的类型的构造函数封装来简化操作,比如,我们要在上一节的例子中,传递一个要显示的字符串给 DoHelloworld 这个作业函数,我们就直接用下面的代码即可:

【Delphi】

Workers.Post(DoHelloworld,TQJobExtData.Create('字符串内容'),True,jdfFreeAsObject);

【C++ Builder】

Workers->Post(DoHelloworld,new TQJobExtData('字符串内容'),true,jdfFreeAsObject);

那么,我们在作业中,如何访问参数的值呢?大家可能已经注意到了对应的 AsXXX 属性,根据对应的类型,使用它即可直接访问了。当然,如果是你自定义的结构体或对象,那么只好老老实实的访问 Origin 指针来得到原始的内容了。

接下来的问题是:如果我们要传复杂的结构体给作业时怎么办?一个程序用到的结构体可能远不止6种,jdfFreeAsC1~jdfFreeAsC6 显然是无法满足需要的。在刚才的 TQJobExtData 声明中,大家可以看到,其包含了一个带有自定义释放函数的形式,实际上只要传递释放内存的代码给它就搞定了。

下面是一个简单的例子:

【Delphi】

type
   TMyComplexRecord=record
     Id:Integer;
     Name:String;
     Actions:array of TAction;
   end;
   PMyComplexRecord=^TMyComplexRecord;
...

procedure TForm1.DoJob(AJob:PQJob);
var
  R:PMyComplexRecord;
begin
R:=AJob.ExtData.Origin;
OutputDebugString(PChar(R.Name));
end;

procedure TForm1.DoFreeMyComplexRecord(AData:Pointer);
begin
Dispose(PMyComplexRecord(AData));
end;

procedure TForm1.Button1Click(Sender:TObject);
var
  AData:PMyComplexRecord;
begin
New(AData);
AData.Name:='Hello';
SetLength(AData.Actions,1);
Workers.Post(DoJob,TQJobExtData.Create(AData,DoFreeMyComplexRecord),false,jdfFreeAsObject);
end;

【C++ Builder】

struct TMyComplexRecord
  {
  int Id;
  String Name;
  TAction Actions[1];
  };
typedef TMyComplexRecord * PMyComplexRecord;
...
void __fastcall TForm1::DoJob(PQJob AJob)
{
OutputDebugString(((PMyComplexRecord)AJob.ExtData.Origin)->Name.c_str());
}

void __fastcall TForm1::DoFreeMyComplexRecord(void *AData)
{
delete (PMyComplexRecord)AData;
}

void __fastcall TForm1::Button1Click(System::TObject *Sender)
{
PMyComplexRecord AData=new TMyComplexRecord;
AData->Name=L"Hello";
Workers->Post(DoJob,new TQJobExtData(AData,DoFreeMyComplexRecord),false,jdfFreeAsObject);
}

好了,上面的一切看起来比较完美了。在结束之前,有的朋友可能说,你自动管理内容的释放虽然好,但是,我如果一个参数要在多个作业间顺序传递,你这个作业完成时就给释放了,害我不得不重建一个拷贝,这个不太好吧?

这是个问题,不过好在我们已经提供了比较完美的解决方案,那就是使用基于接口的智能指针。这本来是另一篇文章的内容,我给内容拷贝过来,比大家参考(原文链接 ):

用 QString 单元为您提供的 TQPtr 做智能指针封装,然后作业时使用 jdfFreeAsInterface 让 QWorker 采用接口的方式来管理附加的数据。

我们先看一下声明:

IQPtr = interface(IInterface)
    function Get: Pointer;
  end;
  TQPtrFreeEvent=procedure (AData:Pointer) of object;
  TQPtrFreeEventG=procedure (AData:Pointer);
  {$IFDEF UNICODE}
  TQPtrFreeEventA=reference to procedure (AData:Pointer);
  {$ENDIF}
  TQPtr = class(TInterfacedObject, IQPtr)
  private
    FObject: Pointer;
    FOnFree:TQPtrFreeEvent;
    {$IFDEF UNICODE}
    FOnFreeA:TQPtrFreeEventA;
    {$ENDIF}
  public
    constructor Create(AObject: Pointer); overload;
    destructor Destroy; override;
    class function Bind(AObject: TObject): IQPtr;overload;
    class function Bind(AData:Pointer;AOnFree:TQPtrFreeEvent):IQPtr;overload;
    class function Bind(AData:Pointer;AOnFree:TQPtrFreeEventG):IQPtr;overload;
    {$IFDEF UNICODE}
    class function Bind(AData:Pointer;AOnFree:TQPtrFreeEventA):IQPtr;overload;
    {$ENDIF}
    function Get: Pointer;
  end;

我们会用到其中的 TQPtr.Bind 函数和 IQPtr 的 Get 接口:

(1)Bind 函数用于将对象或其它类型的指针通过接口的方式进行管理,它有四种重载,如果是对象,不需要指定释放方法,如果是其它的如记录类型,就需要自己指定内存释放函数,后面三个分别对应类成员、全局函数和匿名函数版本,不进行赘述。

(2)Get 函数用于获取到咱们管理的原始的指针。

下面我们提供一个具体的例子来看一下它的用法:

unit Unit1;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs,QString,QWorker, Vcl.StdCtrls;

type
  TMyObject=class
  public
    Text:String;
    destructor Destroy;override;
  end;
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    { Private declarations }
    procedure DoStep1(AJob:PQJob);
    procedure DoStep2(AJob:PQJob);
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
begin
Workers.Post(DoStep1,Pointer(TQPtr.Bind(TMyObject.Create)),False,jdfFreeAsInterface);
end;

procedure TForm1.DoStep1(AJob: PQJob);
var
  AObj:TMyObject;
begin
AObj:=IQPtr(AJob.Data).Get;
AObj.Text:='Hello,world from DoStep1';
Workers.Post(DoStep2,AJob.Data,True,jdfFreeAsInterface);
end;

procedure TForm1.DoStep2(AJob: PQJob);
var
  AObj:TMyObject;
begin
AObj:=IQPtr(AJob.Data).Get;
ShowMessage(AObj.Text);
end;

{ TMyObject }

destructor TMyObject.Destroy;
begin
  OutputDebugString('Object Free');
  inherited;
end;

end.

上面的这段代码我们投寄了一个两步作业,第一步作业设置了 TMyObject 的一个实例的 Text 值,而第二步作业则将其用 ShowMessage 显示出来,我们没有自己释放 TMyObject 实例,但我们执行上面的代码,我们可以看到结果,截个图看下:

FreeAsInterface

InfFree

可以看到,在后续步骤中,参数被安全的访问,而TMyObject对象的实例被自动释放了。

好了,看起来一切都很完美。传参数的问题解决了,也不需要做太多额外的工作。但我还是想补充一点点,那就是实际上,如果需要,你可以通过 TQMsgPack 或者 TList 一类的对象来传递参数,反正只要指定了 jdfFreeAsObject,在作业生命周期结束时,QWorker 都会帮你自动释放它。所以,本章的内容,只是给的参考,并不代表必需这么做,条条大路通罗马,何必单恋一枝花~~~,词有点串了:),本节内容结束。

 

分享到: