Delphi和线程:"系统错误。代码:1400。窗口句柄无效"

时间:2016-04-26 15:47:31

标签: multithreading delphi thread-safety

有点新线程我遇到了问题:

我为Synapse THTTPSend对象构建了一个小包装器,通过线程处理异步调用。一切似乎都顺利,直到我退出应用程序并得到此错误(使用madExcept异常处理程序)"系统错误。代码:1400。窗口句柄无效。"

main thread ($2d00):
0047f931 +091 x.exe  System.SysUtils          RaiseLastOSError
0047f88e +00e x.exe  System.SysUtils          RaiseLastOSError
006198c4 +064 x.exe  Vcl.Controls             TWinControl.DestroyWindowHandle
0061674c +0dc x.exe  Vcl.Controls             TWinControl.Destroy
0067487b +05b x.exe  Vcl.ComCtrls             TTabSheet.Destroy
00616781 +111 x.exe  Vcl.Controls             TWinControl.Destroy
00673218 +0b8 x.exe  Vcl.ComCtrls             TCustomTabControl.Destroy
0067529c +06c x.exe  Vcl.ComCtrls             TPageControl.Destroy
00616781 +111 x.exe  Vcl.Controls             TWinControl.Destroy
0073d95e +06e x.exe  Vcl.Forms                TScrollingWinControl.Destroy
0073f5d2 +1e2 x.exe  Vcl.Forms                TCustomForm.Destroy
0040b2d5 +015 x.exe  System                   TObject.Free
005a034e +08e x.exe  System.Classes           TComponent.DestroyComponents
0073be06 +046 x.exe  Vcl.Forms                DoneApplication
00472520 +030 x.exe  System.SysUtils          DoExitProc
0040e0d9 +079 x.exe  System                   @Halt0

我已将此跟踪到访问列表视图,它是这样的:

  • GUI在我的包装器中调用proc并分配回调方法
  • Wrapper创建一个线程并设置一个回调
  • 线程完成其工作(http post)然后调用包装器的回调
  • Wrapper的回调触发GUI中的另一个回调,然后更新列表视图中的某些项目

如果我跳过listview部分,错误永远不会发生,所以我认为我的线程代码中的某些内容可能是错误的,这与vcl / gui混淆,可能导致它在访问VCL时仍在运行?如果我检查列表视图,在线程结束后有一些非常奇怪的东西,有时列表视图甚至不可见,或者添加的项目不可点击。

Listview部分

procedure Tx.AddLog(url,DelURL: string);
begin
  if Settings.OptEnableLogging.Checked then begin
    With UploadsForm.ListView1.Items.Add do begin
      Caption := DateTimeToStr(Now);
      SubItems.Add(OriginalFilename);
      SubItems.Add(url);
      SubItems.Add('');
      SubItems.Add(DelURL);
    end;
    SaveLoggingLog;
  end;

    With UploadsForm.ListView2.Items.Add do begin
      Caption := DateTimeToStr(Now);
      SubItems.Add(OriginalFilename);
      SubItems.Add(url);
      SubItems.Add('');
      SubItems.Add(DelURL);
    end;
end;

线程对象

type
  TMySynHTTPAsync = class(TThread)
  protected
    procedure Execute; override;
  private
    sObj: TSynHTTP;
  public
    Constructor Create(SynObj: TSynHTTP);
    Destructor Destroy; override;
end;

implementation

Constructor TMySynHTTPAsync.Create(SynObj: TSynHTTP);
begin
  inherited Create(False);
  Self.FreeOnTerminate := True;
  sObj := SynObj;
end;

Destructor TMySynHTTPAsync.Destroy;
begin
  //
  inherited Destroy;
end;

Procedure TMySynHTTPAsync.Execute;
begin
  With sObj do begin
    try
      case tCallType of
        thPostString: ThreadResult := sObj.Post(tURL, tPostVars);
      end;
    except
      //
    end;
    if Assigned(sObj.xOnAsyncRequestDone) then sObj.xOnAsyncRequestDone;
    FThread := nil;
  end;
end;

创建线程

FThread: TThread;

procedure TSynHTTP.DoAsync;
begin
  ThreadResult := False;
  FThread := TMySynHTTPAsync.Create(Self);
  FThread.Resume;
end;

我猜这是罪魁祸首,因为它在线程完成之前经历了所有的GUI处理。

  

如果已分配(sObj.xOnAsyncRequestDone),则sObj.xOnAsyncRequestDone;

我怎么解决这个问题?

3 个答案:

答案 0 :(得分:4)

你发布了很多代码但不是关键的相关部分。特别是xOnAsyncRequestDone事件处理程序/方法的实现(除非它实际上只调用您发布的日志方法)。

此方法正在 TMySynHTTPAsync 线程的上下文中执行,并且基于您描述的行为 - 尤其是Synchronize解决您的问题的事实 - 很可能是某些该事件处理程序中的活动正在创建一个窗口句柄。

然后,该窗口句柄由 HTTP异步线程拥有,而不是主应用程序线程(有时称为" VCL线程"),否则它将运行您的应用程序。当你的应用程序关闭时,VCL线程执行一些最终的内务处理,销毁对象和窗口等。如果其中一个窗口是由其他一些线程创建的,这将导致问题。

窗口句柄是创建它们的线程的严格属性。您无法在一个线程中创建窗口句柄,然后在另一个线程中销毁它。

注意: 这是Windows的基础,而不是Delphi

值得注意的是,VCL中的窗口句柄通常可以间接创建。您不一定会看到一个标记创建底层窗口句柄的控件的显式创建。窗口句柄仅在需要时才实际创建是很常见的。同样,更改控件的属性可以触发VCL尝试重新创建该控件的窗口,从而破坏当前控件的当前窗口。

应该很明显,这些机制非常容易受到VCL方法被VCL线程以外的线程调用时可能出现的问题的影响。这就是为什么你经常会在这里说" VCL不是线程安全的"。

最安全的操作方法是仅从VCL线程本身运行的代码中操作VCL对象。

与救援同步

这实际上正是Synchronize存在的原因。

使用Synchronize调用的机制实际上可以确保您正在同步的方法在VCL线程上执行。如果这实际上是在创建一个窗口句柄,那么当VCL线程后来破坏该窗口句柄时,可以自由地这样做,因为它实际上创建了它。

因此你的问题就解决了。

其他选项

Synchronize机制非常复杂,但是(这些天)处理跨平台问题等等,因此在这种情况下可能会有点过分。

如果您的代码特定于Windows,则此问题的可能替代解决方案可能是利用Windows允许线程将消息发送(或发布)到其他线程中的窗口这一事实。当那些窗口接收到这些消息时,它们将由该窗口自己的线程处理,就像那些窗口的所有其他消息一样。即你不能最终打断"点击"该窗口收到的消息突然跳过来运行来自线程的通知。当窗口完成处理该点击消息时,该通知消息只需等待它转动。例如。

您可以将其视为“同步”。 system"内置"进入操作系统。

因此,您可以在初始化期间将窗口句柄传递给窗体(或控件或带窗口句柄的任何东西)到HTTP异步线程,识别希望接收"请求完成的VCL窗口#34;或来自该主题的其他通知。然后,线程可以使用PostMessageSendMessage向该窗口句柄发送通知,您可以通过覆盖表单上的 WindowProc 或使用声明的消息处理程序来处理。

如果线程使用SendMessage()发送通知,则它会自动挂起并强制等待,直到窗口收到并处理该消息(在VCL线程中)。

如果线程使用PostMessage(),则消息将异步发送,并且线程可以继续执行其他工作而无需等待。 VCL线程最终将获取消息并进行处理。

不是推荐

这并不是说在这种情况下我会推荐这种替代方案。虽然它看起来似乎是合适的,因为它确实看似简单"工作是完整的"在这种情况下的通知,如果没有更全面地了解您的具体需求,就不可能说哪个最合适。

我提到它只是为了强调替代品确实存在这一事实,安全可靠线程的关键是理解所涉及的原则和机制。

答案 1 :(得分:1)

带有线程的黄金法则是不要从另一个线程触摸GUI。 根据具体情况,可以使用Synchronize()解决此问题,发布消息为异步(PostMessage())或同步(SendMessage())。另一个异步选项是使用TThread.Queue()调用。

最后但并非最不重要的是,如果要通知GUI线程已完成,请为线程分配OnTerminate事件处理程序。当线程完成执行时,该事件在主线程中执行。 这是一个如何实现它的例子:

type
  TMySynHTTPAsync = class(TThread)
  protected
    procedure Execute; override;
  private
    sObj: TSynHTTP;
    procedure MyTerminateHandler(Sender: TObject);
  public
    Constructor Create(SynObj: TSynHTTP);
    Destructor Destroy; override;
end;

procedure TMySynHTTPAsync.MyTerminateHandler(Sender: TObject);
begin // Executed in the main thread
  if Assigned(sObj) and Assigned(sObj.xOnRequestDone) then sObj.xOnRequestDone;
end;

procedure TMySynHTTPAsync.Execute;
begin
  Self.OnTerminate := MyTerminateHandler;  // Assign the OnTerminate event handler
  ...
end;

答案 2 :(得分:0)

同步(sObj.xOnAsyncRequestDone)似乎解决了这个问题。