Переработают ли обработчики событий в Embarcadero C++Builder?
Я хотел бы спросить совета о том, как справиться с проблемой Embarcadero CB10.1 с повторным входом. Скомпилировано в конфигурации отладки, для параметра "Отключить все оптимизации" задано значение true. Я работаю на Win7.
У меня есть простой контрольный пример. Форма с двумя кнопками. Обработчик события OnClick для каждой кнопки вызывает одну и ту же функцию с интенсивным использованием ЦП. Ниже приведен заголовочный файл, за которым следует файл программы.
#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;
}
//---------------------------------------------------------------------------
- 1-й случай: показанный код, но без вызова ProcessMessages().
Когда я запускаю это и нажимаю кнопку 1, загрузка процессора увеличивается почти до 100% в течение примерно 9 секунд. Форма перестает отвечать на запросы в течение этого времени. Не могу переместить форму или нажать на кнопку 2.
Это работает, как я ожидал.
2-й случай: чтобы форма реагировала на пользователя во время функции, интенсивно использующей процессор, я добавил вызов ProcessMessages (), как показано ниже. Теперь я могу перемещать форму и нажимать на другие кнопки.
Это не всегда хорошо, потому что я могу снова нажать кнопку 1 или даже нажать кнопку 2. Любой щелчок снова отключит функцию, интенсивно использующую процессор. Чтобы не запускать функцию, интенсивно использующую процессор, во второй раз, я установил статический логический флаг "InUse". Я устанавливаю это, когда функция запускается, и очищаю ее, когда функция завершается.
Поэтому я проверяю флаг, когда я вхожу в функцию с интенсивным использованием ЦП и, если она установлена (она должна была быть установлена предыдущим нажатием кнопки), я показываю сообщение и затем жду, пока флаг не очистится.
Но флаг никогда не сбрасывается, и моя программа зацикливается на операторе while. Я хотел бы, чтобы программа просто ожидала завершения интенсивной работы процессора, а затем просто снова запустила ее.
Если я установил точку останова в функции Spin() после того, как попал в тупик, она никогда не сработает, указывая, что ни одно из событий не выполняется.
Я знаю, что VCL не является потокобезопасным, но здесь вся обработка происходит в основном потоке. В моем реальном коде есть много обращений к функциям VCL, поэтому интенсивная загрузка ЦП должна оставаться в основном потоке.
Я рассмотрел критические разделы и мьютексы, но поскольку все находится в основном потоке, любое их использование не блокирует.
Может быть, проблема в стеке? Есть ли решение, которое позволяет мне справиться с этим без тупика?
2 ответа
2-й случай: чтобы форма реагировала на пользователя во время функции, интенсивно использующей процессор, я добавил вызов ProcessMessages(), как показано ниже. Теперь я могу перемещать форму и нажимать на другие кнопки.
Это всегда неправильное решение. Правильный способ справиться с этой ситуацией - переместить код с интенсивным использованием ЦП в отдельный рабочий поток, а затем заставить ваши события кнопок запускать новый экземпляр этого потока, если он еще не запущен. Или оставьте поток работающим в цикле, который спит, когда у него нет работы, а затем заставьте каждое событие кнопки сигнализировать о том, что поток просыпается и выполняет свою работу. В любом случае, НИКОГДА не блокируйте основной поток пользовательского интерфейса!
Это не всегда хорошо, потому что я могу снова нажать кнопку 1 или даже нажать кнопку 2. Любой щелчок снова отключит функцию, интенсивно использующую процессор.
Чтобы не запускать функцию, интенсивно использующую процессор, во второй раз, я установил статический логический флаг "InUse". Я устанавливаю это, когда функция запускается, и очищаю ее, когда функция завершается.
Лучшим способом было бы отключить кнопки во время выполнения работы и повторно включить их после завершения. Тогда работа не может быть повторно введена для начала.
Но даже если вы сохраняете свой флаг, ваша функция должна просто выйти, ничего не делая, если флаг уже установлен.
В любом случае, вы должны отобразить пользовательский интерфейс, сообщающий пользователю, когда работа выполняется. Это становится проще в управлении, если работа выполняется в отдельном потоке.
Поэтому я проверяю флаг, когда я вхожу в функцию с интенсивным использованием ЦП и, если она установлена (она должна была быть установлена предыдущим нажатием кнопки), я показываю сообщение и затем жду, пока флаг не очистится.
Но флаг никогда не очищается и
Это потому, что вы просто запускаете бесконечный цикл, который ничего не делает, поэтому он не позволяет коду развиваться дальше. И уж точно не закончить существующую работу и сбросить флаг.
Наименьшее исправление, которое вы можете внести в существующий код, не переписывая его, это изменить CPUIntensive()
использовать return 0
вместо while (InUse) {}
когда InUse
правда. Это позволит позвонить ProcessMessages()
выйти и вернуть управление обратно к предыдущему CPUIntensive()
вызов, который ждет, чтобы закончить работу.
Я знаю, что VCL не является потокобезопасным, но здесь вся обработка происходит в основном потоке.
Это большая ошибка.
В моем реальном коде есть много обращений к функциям VCL, поэтому интенсивная загрузка ЦП должна оставаться в основном потоке.
Это недостаточно веская причина для выполнения работы в главном потоке. Переместите его в рабочий поток, где он принадлежит, и синхронизируйте его с основным потоком всякий раз, когда ему необходим доступ к пользовательскому интерфейсу. Делайте как можно больше работы в рабочем потоке и синхронизируйте только тогда, когда это абсолютно необходимо.
Мой вопрос был не о потоках, а о том, как предотвратить одновременное нажатие нескольких кнопок, при этом форма не переставала отвечать. Все это в моей однопоточной программе 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, и ожидающие события кнопки мыши запускаются.