【问题标题】:Are event handlers re-entrant in Embarcadero C++Builder?Embarcadero C++Builder 中的事件处理程序是否可重入?
【发布时间】:2017-01-09 01:13:22
【问题描述】:

我想就如何处理 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;
}
//---------------------------------------------------------------------------

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

当我运行它并单击按钮 1 时,CPU 使用率几乎跳到 100% 约 9 秒。在此期间,表单变得无响应。 无法移动表单或单击按钮 2。

效果如我所料。

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

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

所以我在进入 CPU 密集型功能时检查标志并 如果它已设置(它必须是通过先前单击按钮设置的),我 显示一条消息,然后等待标志清除。

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

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

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

我考虑过关键部分和互斥体,但因为一切都在 主线程,任何使用它们都不会阻塞。

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

【问题讨论】:

  • 这是一个单线程程序,所以如果你说while (flag) { };当然会永远循环;其他代码都没有运行,那么flag 的值怎么会改变呢?您必须从嵌套调用返回,以便外部调用可以完成。
  • 如果您真的希望操作在用户单击按钮时运行多次,则必须有一个计数器。但在操作完成之前禁用这两个按钮可能更明智。
  • 说真的,这里真正的问题是您不了解基于消息的 Windows 应用程序是如何工作的。一旦您花时间了解发生了什么,剩下的问题就会自行回答。

标签: c++ events c++builder reentrancy c++builder-10.1-berlin


【解决方案1】:

第二种情况:为了让表单在 CPU 密集型功能期间响应用户,我添加了 ProcessMessages() 调用,如图所示。现在,我可以移动表单并单击其他按钮。

这总是错误的解决方案。处理这种情况的正确方法是将 CPU 密集型代码移动到单独的工作线程,然后让您的按钮事件启动该线程的新实例(如果它尚未运行)。或者,让线程在没有工作时休眠的循环中运行,然后让每个按钮事件通知线程唤醒并完成其工作。无论哪种方式,永远不要阻塞主 UI 线程!

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

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

更好的方法是在执行工作时禁用按钮,并在完成后重新启用它们。那么作品就不能重新开始了。

但是,即使你保留你的标志,如果标志已经设置,你的函数应该直接退出而不做任何事情。

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

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

但标志永远不会清除并且

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

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

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

这是一个大错误。

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

这不是在主线程中执行工作的充分理由。将其移至它所属的工作线程,并在需要访问 UI 时使其与主线程同步。在工作线程中做尽可能多的工作,并仅在绝对必要时进行同步。

【讨论】:

    【解决方案2】:

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

    我需要一种方法来延迟处理鼠标按钮事件,同时处理消息,以使表单不会出现无响应。

    ProcessMessages() 在这里不起作用。它分派在消息队列中找到的每条消息。

    我找到了一种方法,一种检查消息队列的 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 线程消息泵,并触发等待的鼠标按钮事件。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-03-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-06
      • 2013-08-04
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多