在Delphi 2010中恢复挂起的线程?

17

D2010中TThread的resume方法已经被弃用。因此,我认为现在应该这样使用:

TMyThread = class (TThread)
protected
  Execute; override;
public
  constructor Create;
end;
...

TMyThread.Create;
begin
  inherited Create (True);
  ...
  Start;
 end;

不幸的是,我收到了一个异常"Cannot call start on a running or supsended thread"。这对我来说很奇怪,因为文档告诉我应该在以挂起模式创建的线程上调用Start。

我在这里错过了什么?

2个回答

19

问题的原因在于Thread不应该自己启动。

线程永远不知道初始化何时完成。构造函数与初始化不同(构造函数应该始终简短且无异常;进一步的初始化在构造函数之后进行)。

相似的情况是TDataSet:任何一个TDataSet构造函数都不应该调用Open或将Active设置为True。

请参阅Wings of Wind的博客文章

你应该采取以下措施之一:

  • 通过调用Create(true)创建一个被挂起的TMyThread,并在TMyThread类之外执行Start。
  • 创建非挂起的TMyThread,确保Create构造函数进行完整的初始化,并让TThread.AfterConstruction启动线程。

TThread用法说明:

基本上,线程应该只是代码执行的上下文封装。

实际执行的业务逻辑代码应该在其他类中。

通过解耦这两者,您可以获得很大的灵活性,特别是从多个地方启动业务逻辑(在编写单元测试时非常方便!)。

这是您可以使用的框架类型:

unit DecoupledThreadUnit;

interface

uses
  Classes;

type
  TDecoupledThread = class(TThread)
  strict protected
    //1 called in the context of the thread
    procedure DoExecute; virtual;
    //1 Called in the context of the creating thread (before context of the new thread actualy lives)
    procedure DoSetUp; virtual;
    //1 called in the context of the thread right after OnTerminate, but before the thread actually dies
    procedure DoTearDown; virtual;
  protected
    procedure DoTerminate; override;
    procedure Execute; override;
  public
    constructor Create;
    procedure AfterConstruction; override;
  end;

implementation

constructor TDecoupledThread.Create;
begin
  // create suspended, so that AfterConstruction can call DoSetup();
  inherited Create(True);
end;

procedure TDecoupledThread.AfterConstruction;
begin
  // DoSetUp() needs to be called without the new thread in suspended state
  DoSetUp();
  // this will unsuspend the underlying thread
  inherited AfterConstruction;
end;

procedure TDecoupledThread.DoExecute;
begin
end;

procedure TDecoupledThread.DoSetUp;
begin
end;

procedure TDecoupledThread.DoTearDown;
begin
end;

procedure TDecoupledThread.DoTerminate;
begin
  inherited DoTerminate();
  // call DoTearDown on in the thread context right before it dies:
  DoTearDown();
end;

procedure TDecoupledThread.Execute;
begin
  // call DoExecute on in the thread context
  DoExecute();
end;

end.

你甚至可以通过以下方式使其基于事件:

unit EventedThreadUnit;

interface

uses
  Classes,
  DecoupledThreadUnit;

type
  TCustomEventedThread = class(TDecoupledThread)
  private
    FOnExecute: TNotifyEvent;
    FOnSetUp: TNotifyEvent;
    FOnTearDown: TNotifyEvent;
  strict protected
    procedure DoExecute; override;
    procedure DoSetUp; override;
    procedure DoTearDown; override;
  public
    property OnExecute: TNotifyEvent read FOnExecute write FOnExecute;
    property OnSetUp: TNotifyEvent read FOnSetUp write FOnSetUp;
    property OnTearDown: TNotifyEvent read FOnTearDown write FOnTearDown;
  end;

  // in case you want to use RTTI
  TEventedThread = class(TCustomEventedThread)
  published
    property OnExecute;
    property OnSetUp;
    property OnTearDown;
  end;

implementation

{ TCustomEventedThread }

procedure TCustomEventedThread.DoExecute;
var
  TheOnExecute: TNotifyEvent;
begin
  inherited;
  TheOnExecute := OnExecute;
  if Assigned(TheOnExecute) then
    TheOnExecute(Self);
end;

procedure TCustomEventedThread.DoSetUp;
var
  TheOnSetUp: TNotifyEvent;
begin
  inherited;
  TheOnSetUp := OnSetUp;
  if Assigned(TheOnSetUp) then
    TheOnSetUp(Self);
end;

procedure TCustomEventedThread.DoTearDown;
var
  TheOnTearDown: TNotifyEvent;
begin
  inherited;
  TheOnTearDown := OnTearDown;
  if Assigned(TheOnTearDown) then
    TheOnTearDown(Self);
end;

end.

或者可以像这样为 DUnit TTestCase 的后代适配:

unit TestCaseThreadUnit;

interface

uses
  DecoupledThreadUnit,
  TestFramework;

type
  TTestCaseRanEvent = procedure (Sender: TObject; const TestResult: TTestResult) of object;
  TTestCaseThread = class(TDecoupledThread)
  strict private
    FTestCase: TTestCase;
  strict protected
    procedure DoTestCaseRan(const TestResult: TTestResult); virtual;
    function GetTestCase: TTestCase; virtual;
    procedure SetTestCase(const Value: TTestCase); virtual;
  protected
    procedure DoExecute; override;
    procedure DoSetUp; override;
    procedure DoTearDown; override;
  public
    constructor Create(const TestCase: TTestCase);
    property TestCase: TTestCase read GetTestCase write SetTestCase;
  end;

implementation

constructor TTestCaseThread.Create(const TestCase: TTestCase);
begin
  inherited Create();
  Self.TestCase := TestCase;
end;

procedure TTestCaseThread.DoExecute;
var
  TestResult: TTestResult;
begin
  if Assigned(TestCase) then
  begin
    // this will call SetUp and TearDown on the TestCase
    TestResult := TestCase.Run();
    try
      DoTestCaseRan(TestResult);
    finally
      TestResult.Free;
    end;
  end
  else
    inherited DoExecute();
end;

procedure TTestCaseThread.DoTestCaseRan(const TestResult: TTestResult);
begin
end;

function TTestCaseThread.GetTestCase: TTestCase;
begin
  Result := FTestCase;
end;

procedure TTestCaseThread.SetTestCase(const Value: TTestCase);
begin
  FTestCase := Value;
end;

procedure TTestCaseThread.DoSetUp;
begin
  if not Assigned(TestCase) then
    inherited DoSetUp();
end;

procedure TTestCaseThread.DoTearDown;
begin
  if not Assigned(TestCase) then
    inherited DoTearDown();
end;

end.

--jeroen


实际上,请看我的编辑:有两种方法可以做到这一点。通常,线程不应该知道它的初始化情况,因为那取决于超出该线程范围的因素。我知道大多数线程类都是针对特定问题的。但它们应该分成两个部分:线程类本身只执行“代码”,而业务类则知道应该以什么顺序执行哪些代码(初始化、主块、完成)。 - Jeroen Wiert Pluimers
2
那个新闻组线程中有264条消息。你能否请做一个总结?当然,一个线程应该知道它的初始化,就像任何其他类应该知道它自己的初始化一样。这就是构造函数的作用。在构造函数运行完成时,该类应该完全准备好使用了。构造函数引发异常没有问题;语言就是为此而设计的。一般来说,线程启动自身没有任何问题。唯一的危险在于早期版本中,但很容易解决。(Uwe的回答和我的评论。) - Rob Kennedy
为什么在TCustomEventedThread.DoExecute等方法中要经过本地变量?这是一种虚假的安全感,因为事件不仅可以被重新分配,对象也可能被销毁,从而使保存的事件处理程序无效。总的来说,我认为基于接口的解决方案是安全地分离线程类和业务逻辑类的唯一方法,因为生命周期管理问题已得到解决。没有必要重新发明轮子,OTL已经存在,并在抽象化这些事物方面走得很远。 - mghie
1
@Ted:我希望Embarcadero论坛服务器有更简单的方法来指向单个消息。我会尝试总结一下:关键在于线程无法启动,直到调用AfterConstruction。但即使在那时,它也不知道初始化是否完成:其他东西可能想要设置一些属性。因此最好将线程与要运行的业务逻辑分离。然后,您可以初始化该逻辑并将完整的内容传递给您的线程。 - Jeroen Wiert Pluimers
@mghie:这是关于锁定的问题。我可以锁定执行完整个DoExecute方法,或者保存一个本地副本。两种方法都有缺点(锁定可能会导致死锁;保留本地副本有底层对象被释放的风险)。从经验来看:即使是基于接口的解决方案,在托管世界中也可能存在过早释放/处理对象的情况。 - Jeroen Wiert Pluimers
显示剩余4条评论

14

简短回答:调用继承的 Create(false) 并省略 Start!

非创建挂起线程的实际启动是在 AfterConstruction 中完成的,该方法在所有构造函数调用后被调用。


2
请注意,这是一个相对较新的开发。旧版本实际上会在继承的构造函数完成运行后立即开始运行。不过有一个非常简单的解决方法:最后调用继承的构造函数。(没有什么理由先调用它;派生类很少需要基本 TThread 构造函数设置的任何属性值。) - Rob Kennedy
不,@Jan,继承的构造函数不能自由地将您的字段设置为零。它无法访问您的字段,因为它是在这些字段甚至不存在之前编写和编译的,因此它无法引用它们。(如果您想到构造函数使用ZeroMemory来清除自身,在任何类中都会导致灾难性后果,而不仅仅是TThread。在任何构造函数运行之前,所有字段都会被清零。请参见TObject.InitInstance。) - Rob Kennedy

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