Delphi的TThread子类返回结果

3

情况。我创建了一个包含一些类的单元来解决代数问题(同余和方程组),以下是代码:

type
 TCongrError = class(Exception)
 end;

type
 TCongruence = class(TComponent)
  //code stuff
  constructor Create(a, b, n: integer); virtual;
 end;

type
 TCongrSystem = array of TCongruence;

type
 TCongruenceSystem = class(TThread)
  private
   resInner: integer;
   FData: TCongrSystem;
   function modinv(u, v: integer): integer; //not relevant
  protected
   procedure Execute; override;
  public
   constructor Create(data: TCongrSystem; var result: integer; hasClass: boolean);
 end;

我决定使用 TThread,因为这个类有一个Execute方法,由于构造函数传递的参数长度较长,可能需要一些时间才能完成。下面是实现代码:

constructor TCongruenceSystem.Create(data: TCongrSystem; var result: integer; hasClass: boolean);
begin

 inherited Create(True);
 FreeOnTerminate := true;

 FData := data;
 setClass := hasClass;
 resInner := result;

end;

procedure TCongruenceSystem.Execute;
var sysResult, i, n, t: integer;
begin

 sysResult := 0;
 n := 1;

 //computation

 Queue( procedure
        begin
         ShowMessage('r = ' + sysResult.ToString);
         resInner := sysResult;
        end );

end;

问题

如果您查看Queue,您会发现我正在使用(仅作为测试)ShowMessage,并显示sysResult的正确值。顺便说一句,第二行存在一些问题,我无法理解。

构造函数有var result:integer,因此我可以从传递的变量中获得副作用,然后我可以分配resInner:= result;。最后(在队列中),我将resInner赋予sysResult的值,并且由于var的副作用,我期望result也会被更新。为什么不会发生这种情况?

我进行了另一个测试,将构造函数更改如下:

constructor TCongruenceSystem.Create(data: TCongrSystem; result: TMemo; hasClass: boolean);
//now of course I have resInner: TMemo

将队列更改为这样:

Queue( procedure
        begin
         ShowMessage('r = ' + sysResult.ToString);
         resInner.Lines.Add(sysResult.ToString);
        end ); //this code now works properly in both cases! (showmessage and memo)

在构造函数中,我传递的是一个引用 TMemo,这没问题,但是原始的 var result: integer 也被作为引用传递了,为什么它不起作用呢?
我想这样做是因为我想要做类似这样的事情:
 //I put var a: integer; inside the public part of the TForm
 test := TCongruenceSystem.Create(..., a, true);
 test.OnTerminate := giveMeSolution;
 test.Start;
 test := nil;

在这里,giveMeSolution是一个简单的过程,它使用包含系统结果的变量a。如果不可能实现这一点,我该怎么办?基本上,在执行结束时的结果只是一个整数,必须传递给主线程。

我已经阅读了关于ReturnValue的内容,但我不确定如何使用它。


这个设计很糟糕,你应该尽早修复它。当然,这里没有任何东西应该从TComponent派生出来。那是毫无意义的。更大的问题是在组件中构建线程。这应该由代码的使用者处理。 - David Heffernan
@DavidHeffernan 感谢您的评论。我想用这个来制作一个组件;看了您的建议,我认为应该删除线程相关的内容,只需添加一个执行系统求解的类函数即可。 - Alberto Miola
1
这个组件是没有意义的。它是一个数字类。在设计界面上不需要它。 - David Heffernan
2个回答

3
假设有一个类字段 FFoo : integer; ;
 procedure TFoo.Foo(var x : integer);
 begin
   FFoo := x;
 end;

在这里,你正在将x赋给FFoo。在Foo方法内部,你可以自由地修改作为x传入的变量的值,但integers是通过赋值复制的值类型。如果你想保留对外部integer变量的引用,你需要将FFoo(或者在你的情况下,resInner)声明为PInteger(指向整数的指针)。例如(简化):
TCongruenceSystem = class(TThread)
  private
    resInner: PInteger;       
  protected
    procedure Execute; override;
  public
    constructor Create(result: PInteger);
end;

在哪里

constructor TCongruenceSystem.Create(result: PInteger);
begin    
  inherited Create(True);
  FreeOnTerminate := true;      
  resInner := result;    
end;

您可以将其称为test:= TCongruenceSystem.Create(@a);并进行赋值:

 { ** See the bottom of this answer for why NOT to use }
 {    Queue with FreeOnTerminate = true **             }
 Queue( procedure
    begin
      ShowMessage('r = ' + sysResult.ToString);
      resInner^ := sysResult;
    end );

它能与 TMemo 一起使用的原因是类是引用类型 - 它们的变量不保存值,而是指向内存中对象的地址。当您复制类变量时,只会复制引用(即指针),而对于值类型,则在赋值时复制变量的内容。

话虽如此,你仍然可以将参数类型定义为 var x : integer 并在构造函数中 获取引用

constructor TCongruenceSystem.Create(var result: Integer);
begin    
  inherited Create(True);
  FreeOnTerminate := true;      
  resInner := @result;   {take the reference here}   
end;

但是这会让调用者误以为一旦构造函数完成,他们就可以自由地处理整数并对其进行任何修改。显式地传递 PInteger 给调用者一个提示,即您的对象将保留对他们提供的整数的引用,并且需要确保底层变量在您的类存在期间保持有效。
而且...尽管如此,我仍然根本不喜欢这个想法。通过像这样接收变量引用,您正在将一个非典型的生命周期管理问题转嫁给调用者。传递指针最好只在传输点处使用。持有外部指针很混乱,错误很容易发生。在这里更好的方法是提供完成事件,让您的类的使用者附加处理程序。
例如:
  { define a suitable callback signature }
  TOnCalcComplete = procedure(AResult : integer) of object;

  TCongruenceSystem = class(TThread)
  private
    Fx, Fy : integer;
    FOnCalcComplete : TOnCalcComplete;
  protected
    procedure Execute; override;
  public
    constructor Create(x,y: integer);
    property OnCalcComplete : TOnCalcComplete read FOnCalcComplete write FOnCalcComplete;
  end;

constructor TCongruenceSystem.Create(x: Integer; y: Integer);
begin
  inherited Create(true);
  FreeOnTerminate := true;
  Fx := x;
  Fy := y;
end;

procedure TCongruenceSystem.Execute;
var
  sumOfxy : integer;
begin
  sumOfxy := Fx + Fy;
  sleep(3000);        {take some time...}
  if Assigned(FOnCalcComplete) then
    Synchronize(procedure
                begin
                  FOnCalcComplete(sumOfxy);
                end);
end;

然后你会调用它:
{ implement an event handler ... }
procedure TForm1.CalcComplete(AResult: Integer);
begin
  ShowMessage(IntToStr(AResult));
end;

procedure TForm1.Button1Click(Sender: TObject);
var
  LCongruenceSystem : TCongruenceSystem;
begin
  LCongruenceSystem := TCongruenceSystem.Create(5, 2);
  LCongruenceSystem.OnCalcComplete := CalcComplete;  { attach the handler }
  LCongruenceSystem.Start;
end;

你还会注意到我在这里使用了Synchronize而不是Queue。关于这个话题,请阅读这个问题(我引用Remy的话): 确保所有TThread.Queue方法在线程自毁之前完成

在排队的方法中设置FreeOnTerminate := True会导致内存泄漏。


非常感谢,我明白了我的错误!我以为使用 var 可以复制值和引用,但实际上只能复制值,我错了。 - Alberto Miola

3
基本上,在Execute结束时的结果只是一个整数,必须传递给主线程。
我已经阅读了有关ReturnValue的内容,但我不确定如何使用它。
使用ReturnValue属性非常容易:
type
  TCongruenceSystem = class(TThread)
    ... 
  protected
    procedure Execute; override;
  public
    property ReturnValue; // protected by default
  end;

procedure TCongruenceSystem.Execute;
var
 ...
begin
  // computation
  ReturnValue := ...;
end;

test := TCongruenceSystem.Create(...);
test.OnTerminate := giveMeSolution;
test.Start;

....

procedure TMyForm.giveMeSolution(Sender: TObject);
var
  Result: Integer;
begin
  Result := TCongruenceSystem(Sender).ReturnValue;
  ...
end;

谢谢,这就是我要找的。返回值只接受整数吗?还是可以接受例如记录之类的数据? - Alberto Miola
@AlbertoMiola 只有一个整数。如果您想返回更复杂的数据,最好的方法是通过自定义事件,就像我在我的答案中所示的那样。 - J...
@AlbertoMiola:或者至少通过线程类的自定义字段/属性。 - Remy Lebeau
@J...:这对OnTerminate事件处理程序没有影响,该处理程序通过Synchronize()在线程对象被销毁之前触发。事件处理程序将能够访问线程对象的字段/属性(如我的答案所示)。即使是您自己的答案也利用了这一点。 - Remy Lebeau
@RemyLebeau 当然,抱歉。 我习惯于丢弃对于“Free on Terminate”线程的引用。 把在Sender中返回一个是我忘了的。 完全有效。 - J...

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接