для этой задачи вам понадобится что-то вроде Run-Down Защита вместо CPendingOperationGuard
перед началом работы вы вызываете ExAcquireRundownProtection
< /a> и только если он вернет TRUE - начать выполнение операции. в конце вы должны вызвать ExReleaseRundownProtection
< /а>
поэтому шаблон должен быть следующим
if (ExAcquireRundownProtection(&RunRef)) {
do_operation();
ExReleaseRundownProtection(&RunRef);
}
когда вы хотите остановить этот процесс и дождаться завершения всех активных вызовов do_operation();
, вы звоните ExWaitForRundownProtectionRelease
(вместо WaitWorker
)
После вызова ExWaitForRundownProtectionRelease
подпрограмма ExAcquireRundownProtection
вернет значение FALSE (поэтому новые операции после этого запускаться не будут). ExWaitForRundownProtectionRelease
ожидает возврата до тех пор, пока all не вызовет процедуру ExReleaseRundownProtection
для сброса ранее полученной защиты от выбега (поэтому, когда все текущие (если существуют) операции завершатся). Когда все незавершенные обращения завершены, ExWaitForRundownProtectionRelease
возвращает значение
к сожалению, этот API реализуется системой только в режиме ядра и не имеет аналога в пользовательском режиме. однако не сложно реализовать такую идею самостоятельно
это мой пример:
enum RundownState {
v_complete = 0, v_init = 0x80000000
};
template<typename T>
class RundownProtection
{
LONG _Value;
public:
_NODISCARD BOOL IsRundownBegin()
{
return 0 <= _Value;
}
_NODISCARD BOOL AcquireRP()
{
LONG Value, NewValue;
if (0 > (Value = _Value))
{
do
{
NewValue = InterlockedCompareExchangeNoFence(&_Value, Value + 1, Value);
if (NewValue == Value) return TRUE;
} while (0 > (Value = NewValue));
}
return FALSE;
}
void ReleaseRP()
{
if (InterlockedDecrement(&_Value) == v_complete)
{
static_cast<T*>(this)->RundownCompleted();
}
}
void Rundown_l()
{
InterlockedBitTestAndResetNoFence(&_Value, 31);
}
void Rundown()
{
if (AcquireRP())
{
Rundown_l();
ReleaseRP();
}
}
RundownProtection(RundownState Value = v_init) : _Value(Value)
{
}
void Init()
{
_Value = v_init;
}
};
///////////////////////////////////////////////////////////////
class OperationGuard : public RundownProtection<OperationGuard>
{
friend RundownProtection<OperationGuard>;
HANDLE _hEvent;
void RundownCompleted()
{
SetEvent(_hEvent);
}
public:
OperationGuard() : _hEvent(0) {}
~OperationGuard()
{
if (_hEvent)
{
CloseHandle(_hEvent);
}
}
ULONG WaitComplete(ULONG dwMilliseconds = INFINITE)
{
return WaitForSingleObject(_hEvent, dwMilliseconds);
}
ULONG Init()
{
return (_hEvent = CreateEvent(0, 0, 0, 0)) ? NOERROR : GetLastError();
}
} g_guard;
//////////////////////////////////////////////
ULONG CALLBACK PendingOperationThread(void*)
{
while (g_guard.AcquireRP())
{
Sleep(1000);// do operation
g_guard.ReleaseRP();
}
return 0;
}
void demo()
{
if (g_guard.Init() == NOERROR)
{
if (HANDLE hThread = CreateThread(0, 0, PendingOperationThread, 0, 0, 0))
{
CloseHandle(hThread);
}
MessageBoxW(0, 0, L"UI Thread", MB_ICONINFORMATION|MB_OK);
g_guard.Rundown();
g_guard.WaitComplete();
}
}
зачем просто ждать, когда ждать, пока m_ullCounter
не станет нулем, недостаточно
если мы читаем 0 из m_ullCounter
, это означает только то, что в это время нет активной операции. но отложенная операция может начаться уже после того, как мы проверим, что m_ullCounter == 0
. мы можем использовать специальный флаг (скажем, bool g_bQuit
) и установить его. перед началом операции проверьте этот флаг и не начинайте, если он истинен. но этого в любом случае недостаточно
наивный код:
// рабочий поток
if (!g_bQuit) // (1)
{
// MessageBoxW(0, 0, L"simulate delay", MB_ICONWARNING);
InterlockedIncrement(&g_ullCounter); // (4)
// do operation
InterlockedDecrement(&g_ullCounter); // (5)
}
// здесь ждем завершения всей операции
g_bQuit = true; // (2)
// wait on g_ullCounter == 0, how - not important
while (g_ullCounter) continue; // (3)
- ожидание операции проверки флага g_bQuit (1) - пока ложно, поэтому начинается
- рабочий поток заменен (используйте MessageBox для имитации этого)
- устанавливаем g_bQuit = true; // (2)
- мы проверяем/ждем
g_ullCounter == 0
, это 0, поэтому мы выходим (3)
- пробуждение рабочего потока (возврат из MessageBox) и приращение
g_ullCounter
(4)
проблема здесь в том, что операция может использовать некоторые ресурсы, которые мы уже начинаем уничтожать после g_ullCounter == 0
это происходит из-за проверки флага выхода (g_Quit) и счетчика приращения после этого не атомарный - между ними может быть пробел.
для правильного решения нам нужен атомарный доступ к флагу + счетчику. это и сделать защиту от износа. для флага+счетчика используется одна переменная LONG (32 бита), потому что мы можем сделать к ней атомарный доступ. 31 бит используется для счетчика и 1 бит используется для флага выхода. Решение для Windows использует 0 бит для флага (1 означает выход) и [1..31] бит для счетчика. я использую биты [0..30] для счетчика и 31 бит для флага (0 означает выход). искать
10.04.2020
m_ullCounter
станет 0, недостаточно для задачи. предположим, что мы получили этоm_ullCounter == 0
. и что ? новая операция может начаться уже после того, какm_ullCounter
станет 0 11.04.2020InterlockedExchangeAdd64((LONG64 volatile *)&m_ullCounter, 0);
существует смысл использовать только для операции ReadModifyWrite. это не имеет смысла только для чтения, если процессор может выполнять чтение как атомарное (выровненное 32-битное целочисленное атомарное чтение на x86/x64 и думать также на любой платформе), поэтому достаточно просто прочитатьm_ullCounter
11.04.2020