EnterCriticalSection Deadlock
Наличие, как представляется, ситуации тупиковой блокировки с многопоточным приложением регистрации.
Немного предыстории:
Мое основное приложение имеет 4-6 потоков. Основной поток, отвечающий за мониторинг работоспособности различных вещей, которые я делаю, обновление графического интерфейса и т. Д. Затем у меня есть поток передачи и поток приема. Передающие и принимающие потоки взаимодействуют с физическим оборудованием. Иногда мне нужно отлаживать данные, которые видят потоки передачи и получения; т.е. печатать на консоль, не прерывая их из-за их критичного ко времени характера данных. Данные, кстати, находятся на шине USB.
Из-за многопоточности приложения я хочу создать консоль отладки, в которую я могу отправлять сообщения из других моих потоков. Получатель отладки работает как поток с низким приоритетом и реализует кольцевой буфер, так что при печати на консоль отладки сообщение быстро сохраняется в кольцевом буфере и устанавливает и событие. Поток консоли отладки хранит события WaitingOnSingleObject из входящих сообщений, которые входят в него. При обнаружении события поток консоли обновляет отображение графического интерфейса с помощью сообщения. Просто а? При вызовах печати и в потоке консоли используется критический раздел для управления доступом.
ПРИМЕЧАНИЕ. Я могу настроить размер кольцевого буфера, если увижу, что я сбрасываю сообщения (по крайней мере, это идея).
В тестовом приложении консоль работает очень хорошо, если я медленно вызываю ее метод Print с помощью щелчков мыши. У меня есть кнопка, которую я могу нажать для отправки сообщений на консоль, и она работает. Однако, если я добавлю какую-либо нагрузку (много вызовов метода Print), все будет заблокировано. Когда я отслеживаю взаимоблокировку, отладчик моей IDE отслеживает EnterCriticalSection и сидит там.
ПРИМЕЧАНИЕ. Если я удаляю вызовы Lock/UnLock и просто использую Enter/LeaveCriticalSection (см. Код), я иногда работаю, но все же оказываюсь в ситуации тупиковой блокировки. Чтобы исключить взаимные блокировки для стека push / pops, я прямо сейчас вызываю Enter / LeaveCriticalSection, но это не решило мою проблему.... Что здесь происходит?
Вот один оператор Print, который позволяет мне передать простой int в консоль дисплея.
void TGDB::Print(int I)
{
//Lock();
EnterCriticalSection(&CS);
if( !SuppressOutput )
{
//swprintf( MsgRec->Msg, L"%d", I);
sprintf( MsgRec->Msg, "%d", I);
MBuffer->PutMsg(MsgRec, 1);
}
SetEvent( m_hEvent );
LeaveCriticalSection(&CS);
//UnLock();
}
// My Lock/UnLock methods
void TGDB::Lock(void)
{
EnterCriticalSection(&CS);
}
bool TGDB::TryLock(void)
{
return( TryEnterCriticalSection(&CS) );
}
void TGDB::UnLock(void)
{
LeaveCriticalSection(&CS);
}
// This is how I implemented Console's thread routines
DWORD WINAPI TGDB::ConsoleThread(PVOID pA)
{
DWORD rVal;
TGDB *g = (TGDB *)pA;
return( g->ProcessMessages() );
}
DWORD TGDB::ProcessMessages()
{
DWORD rVal;
bool brVal;
int MsgCnt;
do
{
rVal = WaitForMultipleObjects(1, &m_hEvent, true, iWaitTime);
switch(rVal)
{
case WAIT_OBJECT_0:
EnterCriticalSection(&CS);
//Lock();
if( KeepRunning )
{
Info->Caption = "Rx";
Info->Refresh();
MsgCnt = MBuffer->GetMsgCount();
for(int i=0; i<MsgCnt; i++)
{
MBuffer->GetMsg( MsgRec, 1);
Log->Lines->Add(MsgRec->Msg);
}
}
brVal = KeepRunning;
ResetEvent( m_hEvent );
LeaveCriticalSection(&CS);
//UnLock();
break;
case WAIT_TIMEOUT:
EnterCriticalSection(&CS);
//Lock();
Info->Caption = "Idle";
Info->Refresh();
brVal = KeepRunning;
ResetEvent( m_hEvent );
LeaveCriticalSection(&CS);
//UnLock();
break;
case WAIT_FAILED:
EnterCriticalSection(&CS);
//Lock();
brVal = false;
Info->Caption = "ERROR";
Info->Refresh();
aLine.sprintf("Console error: [%d]", GetLastError() );
Log->Lines->Add(aLine);
aLine = "";
LeaveCriticalSection(&CS);
//UnLock();
break;
}
}while( brVal );
return( rVal );
}
MyTest1 и MyTest2 - это всего лишь две тестовые функции, которые я вызываю в ответ на нажатие кнопки. MyTest1 никогда не вызывает проблем, независимо от того, насколько быстро я нажимаю кнопку. MyTest2 блокируется практически каждый раз.
// No Dead Lock
void TTest::MyTest1()
{
if(gdb)
{
// else where: gdb = new TGDB;
gdb->Print(++I);
}
}
// Causes a Dead Lock
void TTest::MyTest2()
{
if(gdb)
{
// else where: gdb = new TGDB;
gdb->Print(++I);
gdb->Print(++I);
gdb->Print(++I);
gdb->Print(++I);
gdb->Print(++I);
gdb->Print(++I);
gdb->Print(++I);
gdb->Print(++I);
}
}
ОБНОВЛЕНИЕ: обнаружена ошибка в моей реализации кольцевого буфера. При большой нагрузке, когда буфер был упакован, я не обнаружил полный буфер должным образом, поэтому буфер не возвращался. Я почти уверен, что проблема сейчас решена. Как только я исправил проблему с кольцевым буфером, производительность стала намного лучше. Однако, если я уменьшу iWaitTime, моя мертвая блокировка (или проблема зависания) вернется.
Так что после дальнейших испытаний с гораздо более тяжелой нагрузкой мой тупик не исчез. Под сверхтяжелой нагрузкой я продолжаю тупиковую блокировку или, по крайней мере, мое приложение зависает, но не там, где оно используется, так как я исправил проблему кольцевого буфера. Если я удвою количество вызовов печати в MyTest2, я легко могу заблокировать каждый раз....
Кроме того, мой обновленный код отражен выше. Я знаю, убедитесь, что мои вызовы событий Set & Reset находятся внутри вызовов критической секции.
3 ответа
Когда эти параметры закрыты, я буду задавать вопросы об этом объекте "Информация". Это окно, к какому окну оно относится, и для какого потока оно было создано?
Если Info или его родительское окно были созданы в другом потоке, может возникнуть следующая ситуация:
Консольная нить находится внутри критической секции, обрабатывающей сообщение. Главный поток вызывает Print() и блокирует критическую секцию, ожидая, когда консольный поток снимет блокировку. Поток консоли вызывает функцию Info (Caption), в результате чего система отправляет сообщение (WM_SETTEXT) в окно. SendMessage блокируется, поскольку целевой поток не находится в состоянии оповещения о сообщении (не блокируется при вызове GetMessage/WaitMessage/MsgWaitForMultipleObjects).
Теперь у вас тупик.
Этот вид #$(%^ может происходить всякий раз, когда вы смешиваете блокирующие подпрограммы с чем-либо, что взаимодействует с окнами. Единственная подходящая блокирующая функция для использования в потоке графического интерфейса - это MSGWaitForMultipleObjects, в противном случае вызовы SendMessage для окон, размещенных в потоке, могут легко зайти в тупик.
Предотвращение этого включает два возможных подхода:
- Никогда не взаимодействовать с GUI в рабочих потоках. Используйте PostMessage только для отправки неблокирующих команд обновления пользовательского интерфейса в поток пользовательского интерфейса, ИЛИ
- Используйте объекты событий ядра + MSGWaitForMultipleObjects (в потоке GUI), чтобы гарантировать, что даже когда вы блокируете ресурс, вы все равно отправляете сообщения.
Не зная, где он находится в тупике, этот код трудно понять. Два комментария
Учитывая, что это C++, вы должны использовать объект Auto для блокировки и разблокировки. На случай, если когда-нибудь станет не катастрофическим для журнала выдать исключение.
Вы сбрасываете событие в ответ на WAIT_TIMEOUT. Это оставляет небольшое окно возможности для второго вызова Print() для установки события, когда рабочий поток вернулся из WaitForMultiple, но до того, как он вошел в критическую секцию. Что приведет к сбросу события, когда на самом деле ожидают данные.
Но вам нужно отладить его и показать, где он находится. Если один поток застрял на EnterCriticalSection, то мы можем выяснить, почему. Если нет ни одной нити, то неполная печать - это просто результат потерянного события.
Я настоятельно рекомендую реализацию без блокировок.
Это не только позволит избежать потенциальной тупиковой ситуации, но и отладка - это то место, где вы абсолютно не хотите брать блокировку. Влияние форматирования сообщений отладки на синхронизацию многопоточного приложения достаточно сильно... наличие блокировок синхронизирует ваш параллельный код только потому, что вы установили, что отладка бесполезна.
То, что я предлагаю, - это дизайн на основе SList (Win32 API обеспечивает реализацию SList, но вы можете достаточно легко создать потокобезопасный шаблон, используя InterlockedCompareExchange и InterlockedExchange). Каждый поток будет иметь пул буферов. Каждый буфер будет отслеживать поток, из которого он пришел, после обработки буфера менеджер журналов отправит буфер обратно в SList исходного потока для повторного использования. Потоки, желающие написать сообщение, опубликуют буфер в потоке регистратора. Это также препятствует тому, чтобы любой поток истощал другие потоки буферов. Событие для пробуждения потока логгера, когда буфер помещен в очередь, завершает проектирование.