3

我想就如何处理重新进入的 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 使用率在大约 9 秒内跃升至几乎 100%。在此期间,表单变得无响应。无法移动表单或单击按钮 2。

这如我所料。

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

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

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

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

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

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

我考虑过关键部分和互斥锁,但由于一切都在主线程中,因此对它们的任何使用都不会阻塞。

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

4

2 回答 2

3

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

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

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

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

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

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

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

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

但旗帜永远不会清除

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

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

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

泰是一个大错误。

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

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

于 2017-01-09T05:55:23.957 回答
0

我的问题不是关于线程,而是如何防止多次单击按钮同时执行,而不是让表单变得无响应。所有这些都在我的单线程 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 线程消息泵,并触发等待的鼠标按钮事件。

于 2017-01-17T23:02:05.443 回答