事件处理程序是Embarcadero C++Builder中的重入程序吗

Are event handlers re-entrant in Embarcadero C++Builder?

本文关键字:程序 C++Builder Embarcadero 事件处理      更新时间:2023-10-16

我想就如何处理Embarcadero CB10.1的重新进入问题征求一些建议。在调试配置中编译,"禁用所有优化"设置为true。我正在运行Win7。

我有一个简单的测试用例。有两个按钮的窗体。每个按钮的OnClick事件处理程序调用相同的CPU密集型函数。下面是头文件,后面是程序文件。

#ifndef Unit1H
#define Unit1H
//---------------------------------------------------------------------------
#include <System.Classes.hpp>
#include <Vcl.Controls.hpp>
#include <Vcl.StdCtrls.hpp>
#include <Vcl.Forms.hpp>
//---------------------------------------------------------------------------
class TForm1 : public TForm
{
__published:    // IDE-managed Components
TButton *Button1;
TButton *Button2;
void __fastcall Button1Click(TObject *Sender);
void __fastcall Button2Click(TObject *Sender);
private:    // User declarations
double __fastcall CPUIntensive(double ButonNo);
double __fastcall Spin(double Limit);
public:     // User declarations
__fastcall TForm1(TComponent* Owner);
};
//---------------------------------------------------------------------------
extern PACKAGE TForm1 *Form1;
//---------------------------------------------------------------------------
#endif

//---------------------------------------------------------------------------
#include <vcl.h>
#pragma hdrstop
#include "Unit1.h"
//---------------------------------------------------------------------------
#pragma package(smart_init)
#pragma resource "*.dfm"
TForm1 *Form1;
//---------------------------------------------------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
: TForm(Owner)
{
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
Button1->Caption = "Pushed";
double retv = CPUIntensive(1);
Button1->Caption = "Button1";
if (retv) ShowMessage("Button1 Done");
}
//---------------------------------------------------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
Button2->Caption = "Pushed";
double retv = CPUIntensive(2);
Button2->Caption = "Button2";
if (retv) ShowMessage("Button2 Done");
}
//---------------------------------------------------------------------------
double __fastcall TForm1::CPUIntensive(double ButtonNo)
{
//
static bool InUse = false;
if (InUse) {
ShowMessage("Reentered by button number " + String(ButtonNo));
while (InUse) {};
}
double retv;
InUse = true;
retv = Spin(30000);         // about 9 seconds on my computer
//retv += Spin(30000);      // uncomment if you have a faster computer
//retv += Spin(30000);
InUse = false;
return retv;
}
//---------------------------------------------------------------------------
double __fastcall TForm1::Spin(double Limit)
{
double k;
for (double i = 0 ; i < Limit ; i++) {
for (double j = 0 ; j < Limit ; j++) {
k = i + j;
// here there can be calls to other VCL functions
Application->ProcessMessages(); // added so UI would be responsive (2nd case)
}
}
return k;
}
//---------------------------------------------------------------------------

-第一种情况:显示的代码没有调用ProcessMessage()。

当我运行这个程序并点击按钮1时,CPU使用率几乎上升到100%持续约9秒。在此期间,表单变得没有响应。无法移动表单或单击按钮2。

正如我所期望的那样。

第二种情况:使表单在CPU期间对用户做出响应密集函数,我添加了ProcessMessage()调用,如图所示。现在,我可以移动表单并点击其他按钮。

这并不总是好的,因为我可以再次点击按钮1或甚至点击按钮2。任何一次点击都会再次启动CPU密集型功能。为了防止CPU密集型函数第二次运行,我制作了一个静态布尔标志"InUse"。我设定了当函数启动时清除,当函数完成时清除。

因此,当我进入CPU密集型功能时,我会检查标志如果它已设置(它一定是通过上次单击按钮设置的)显示一条消息,然后等待标志清除。

但是标志永远不会清除,我的程序在while语句上循环永远我希望程序只等待CPU密集型功能完成,然后再次运行。

如果在遇到死锁后在Spin()函数中设置了断点,它从不触发,表示两个事件都没有执行。

我知道VCL不是线程安全的,但在这里,所有的处理都需要放置在主线程中。在我的实际代码中,有许多调用VCL函数,因此CPU密集型函数必须保留在主函数中线

我考虑过关键部分和互斥,但由于所有内容都在主线程,任何使用它们都不会阻塞。

也许是堆栈问题?有没有一个解决方案可以让我在没有死锁的情况下处理这个问题?

第二种情况:为了使表单在CPU密集型函数期间对用户做出响应,我添加了ProcessMessage()调用,如图所示。现在,我可以移动表单并点击其他按钮。

这总是错误的解决方案。处理这种情况的正确方法是将CPU密集型代码移动到一个单独的工作线程,然后如果该线程尚未运行,则让按钮事件启动该线程的新实例。或者,让线程在一个循环中运行,当它没有工作要做时,这个循环会休眠,然后让每个按钮事件都向线程发出信号,让它醒来并完成它的工作。无论哪种方式,NEVER都不会阻塞主UI线程!

这并不总是好的,因为我可以再次点击按钮1,甚至点击按钮2。任何一次点击都会再次启动CPU密集型功能。

为了防止CPU密集型函数第二次运行,我制作了一个静态布尔标志"InUse"。我在函数启动时设置它,并在函数完成时清除它。

更好的方法是在执行工作时禁用按钮,并在完成时重新启用它们。然后无法重新输入工作。

但是,即使保留了标志,如果已经设置了标志,函数也应该退出而不执行任何操作。

无论哪种方式,都应该显示一个UI,告诉用户工作何时进行。如果工作是在一个单独的线程中完成的,这将变得更容易管理。

因此,当我进入CPU密集型函数时,我会检查标志,如果它已设置(它必须是通过上次单击按钮设置的),我会显示一条消息,然后等待标志清除。

但旗帜永远不会清除,

这是因为你只是在运行一个无休止的循环,它什么都不做,所以它不允许代码继续前进。当然不会完成现有的工作并重置标志。

InUse为true时,您可以在不重新编写的情况下对现有代码进行的最小修复是将CPUIntensive()更改为使用return 0而不是while (InUse) {}。这将允许对ProcessMessages()的调用退出,并将控制权返回到等待完成运行的上一个CPUIntensive()调用。

我知道VCL不是线程安全的,但在这里,所有的处理都在主线程中进行。

这是个大错误。

在我的实际代码中,有许多对VCL函数的调用,因此CPU密集型函数必须保留在主线程中。

这不是在主线程中执行工作的充分理由。将它移动到它所属的工作线程,并在需要访问UI时使它与主线程同步。在工作线程中尽可能多地执行工作,并且只有在绝对必要时才进行同步。

我的问题不是关于线程,而是如何防止多次点击按钮被操作,同时不让表单变得没有响应。所有这些都在我的单线程VCL程序中。正如我所看到的,当我没有对ProcessMessage()的调用时,一旦单击按钮(直到事件处理程序完成处理),表单就会变得没有响应。当我添加对ProcessMessages()的调用时,表单的响应太慢,因为鼠标单击导致事件处理程序运行,即使相同的鼠标单击的事件处理程序在调用ProcessMessage()时只完成了一部分。事件处理程序不是可重新输入的,但当按下第二个鼠标按钮时,Windows/VCL正在重新输入它们。

我需要一种方法来推迟对鼠标按钮事件的处理,同时处理消息,这样表单就不会显得没有响应。

ProcessMessage()在此不起作用。它调度了在消息队列中找到的每一条消息。

我找到了一种方法,ProcessMessages的一个版本检查了消息队列,如果有非鼠标按钮的消息,就发送它。否则,就把消息留在队列中待会儿。

以下是我最终使用的代码,用于替换对ProcessMessages的调用:

// set dwDelay to handle the case where no messages show up
MSG msg;
DWORD dwWait = MsgWaitForMultipleObjects(0, NULL, FALSE, dwDelay, QS_ALLINPUT);
if (dwWait == WAIT_TIMEOUT) {   // Timed out?
// put code here to handle Timeout
return;
}
// Pump the message queue for all messages except Mouse button messages
// from 513 to 521  (0x0201 to 0x0209)
bool MsgAvailable;
while (true) {
MsgAvailable = PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE);
if (!MsgAvailable) break;   // no messages available
if (msg.message <= WM_MOUSEMOVE) {
// Message from WM_NULL to and including WM_MOUSEMOVE
GetMessage(&msg, NULL, WM_NULL, WM_MOUSEMOVE);
TranslateMessage(&msg);
DispatchMessage(&msg);
continue;
}
if (msg.message >= (WM_MOUSELAST+1)) {
// Message from WM_MOUSELAST+1 to the last message possible
GetMessage(&msg, NULL, WM_MOUSELAST+1, 0xFFFFFFFF);
TranslateMessage(&msg);
DispatchMessage(&msg);
continue;
}
// if all that's left is mouse button messages, get out
if (msg.message > WM_MOUSEMOVE || msg.message < WM_MOUSELAST+1) break;
}
return;

现在,事件处理程序可以在不重新输入的情况下完成处理。将处理所有非鼠标按钮事件。事件处理程序完成后,控制返回到主VCL线程消息泵,并触发等待的鼠标按钮事件。