2

我回来了另一个关于线程和同步的问题。想象一个服务器应用程序必须执行一个冗长的操作,而客户端希望他的 GUI 在等待服务器响应时保持响应。我想到了以下模式:

TMonitor.Enter (FTCPClient);
try
  WorkerThread := TWorkerThread.Create (SomeLengthyServerOperation);
  while (not WorkerThread.Ready) do
    Application.ProcessMessages;
  DoSometingWithResults (WorkerThread.Result);
  WorkerThread.Free;      
finally
  TMonitor.Exit (FTCPClient);
end;

WorkerThread 是一个从 TThread 派生的简单类,它执行传递给其构造函数的函数,然后终止(Ready=True,结果在 Result 中)。每当单击按钮时,都会执行呈现的代码。

现在我的问题是:如果我非常快速地单击按钮两次,我会收到一些奇怪的错误,看起来很像服务器和客户端之间的通信以某种方式混淆,我想通过锁定 FTCPClient 对象来避免这种情况。Application.ProcessMessages 执行后的事件处理程序在哪个线程中?TMonitor 的锁是每个线程吗?这是否意味着如果我使用 Application.ProcessMessages 锁定不起作用?

我现在无法更好地解释它。我希望有人明白我的意思。如果没有,请随时提出问题。

编辑:对于按钮的禁用和启用:我对客户端代码一无所知。可能是按钮事件处理程序,也可能是其他东西。基本上我想隐藏客户端代码的锁定。

4

2 回答 2

8

TMonitor 只阻止不同的线程获取锁。正在发生的事情是这样的:通过处理来自锁内的消息,您将回到同一个线程中的同一个函数,这导致了对锁的递归获取。然后,您的代码将创建一个新的工作线程,并重新开始循环。您可以禁用该按钮,以便在工作线程完成之前无法再次单击它。确保在开始处理消息之前禁用该按钮并使用另一个 try..finally 块以确保它被重新启用。根据其余代码的排列方式,您甚至可能不需要锁。

于 2009-02-20T16:00:12.260 回答
3

一些评论:

  1. 您的 WorkerThread 听起来就像您刚刚重新实现了AsyncCalls。使用久经考验的实现可能比自己编写更好(除非您这样做是为了学习效果或因为您有非常特殊的要求)。请查看AsyncCallsOmniThreadLibrary

  2. 锁定应该尽可能短,因此将您的整个反应包装到 Monitor.Enter() 和 Monitor.Exit() 中的按钮单击似乎是错误的。这也不是很容易理解的目的。

  3. 在持有锁时调用 Application.ProcessMessages() 会带来各种令人讨厌的意外。如果您需要防止重新输入代码,通常最好禁用所有 UI 元素作为 OnClick 处理程序执行的第一件事,并在处理程序完成时重新启用它们。还要注意,锁可以从同一个线程多次进入,它们只能实现多个线程的独占访问。

  4. 所有的 VCL 都在主 GUI 线程中执行,因此只有当您从后台线程调用相同的代码时才需要锁定。

  5. 如果你看一下这段代码,你会发现你可以通过在 GUI 线程中工作而不是产生一个工作线程来实现同样的事情。

我已经在 StackOverflow 上多次发布了此链接,但请考虑遵循此列表发布中的建议,以了解在进行多线程编程时要记住的一些事项。它有一些很好的建议,尤其是关于第五点。

编辑:很难准确地说,但你的代码应该是这样的:

procedure TForm1.ActionStartExecute(Sender: TObject);
begin
  ActionStart.Enabled := FALSE;
  fWorkerThread := TWorkerThread.Create (Handle, SomeLengthyServerOperation);
end;

procedure TForm1.ActionStartUpdate(Sender: TObject);
begin
  ActionStart.Enabled := fWorkerThread = nil;
end;

procedure TForm1.WMThreadFinished(var AMsg: TWMThreadFinishedMsg);
begin
  // process results
  fWorkerThread := nil;
end;

TWorkerThread 释放自己,但作为最后一个操作,它向表单发布一条消息(这就是它获取窗口句柄作为参数的原因)。在此消息的处理程序中,您将 fWorkerThread 设置为 nil,以便在下一个空闲循环中重新启用该操作。使用 TAction 意味着您不必关心线程创建是由哪个 UI 元素发起的——它们在线程创建时都将被禁用,并在线程完成后重新启用。不需要锁定,并且在线程处于活动状态时不能创建新线程。

于 2009-02-20T15:55:05.127 回答