有点新线程我遇到了问题:
我为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
我已将此跟踪到访问列表视图,它是这样的:
如果我跳过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;
我怎么解决这个问题?
答案 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;或来自该主题的其他通知。然后,线程可以使用PostMessage
或SendMessage
向该窗口句柄发送通知,您可以通过覆盖表单上的 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)似乎解决了这个问题。