Тупик EnterCriticalSection

Возникновение тупиковой ситуации с многопоточным приложением ведения журнала.

Небольшой фон:

В моем основном приложении работает 4-6 потоков. Основной поток, отвечающий за мониторинг работоспособности различных вещей, которые я делаю, обновление графического интерфейса и т. д. Затем у меня есть поток передачи и поток приема. Потоки передачи и приема взаимодействуют с физическим оборудованием. Иногда мне нужно отлаживать данные, которые видят потоки передачи и приема; то есть печатать на консоль, не прерывая их из-за критического характера данных. Данные, кстати, идут по шине USB.

Из-за многопоточного характера приложения я хочу создать консоль отладки, на которую я могу отправлять сообщения из других моих потоков. Консула отладки работает как поток с низким приоритетом и реализует кольцевой буфер, так что при выводе на консоль отладки сообщение быстро сохраняется в кольцевом буфере и наборах и событиях. Поток консоли отладки получает события WaitingOnSingleObject из входящих связанных сообщений, которые приходят. При обнаружении события поток консоли обновляет отображение графического интерфейса с сообщением. Просто, а? Вызовы печати и поток консоли используют критическую секцию для управления доступом.

ПРИМЕЧАНИЕ. Я могу настроить размер кольцевого буфера, если вижу, что удаляю сообщения (по крайней мере, такова идея).

В тестовом приложении консоль работает очень хорошо, если я медленно вызываю ее метод Print с помощью щелчков мыши. У меня есть кнопка, которую я могу нажать для отправки сообщений на консоль, и она работает. Однако, если я помещаю какую-либо нагрузку (много вызовов метода Print), все блокируется. Когда я отслеживаю взаимоблокировку, отладчик моей IDE отслеживает EnterCriticalSection и остается там.

ПРИМЕЧАНИЕ. Если я удаляю вызовы Lock/UnLock и просто использую Enter/LeaveCriticalSection (см. код), я иногда работаю, но все равно оказываюсь в ситуации тупиковой блокировки. Чтобы исключить тупиковые ситуации для стека push/pop, я прямо сейчас вызываю 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, моя мертвая блокировка (или проблема с зависанием) вернется.

Итак, после дальнейших тестов с гораздо большей нагрузкой оказалось, что мой тупик не исчез. При сверхвысокой нагрузке я продолжаю зависать или, по крайней мере, мое приложение зависает, но это почти не используется, так как я исправил проблему с кольцевым буфером. Если я удвою количество вызовов Print в MyTest2, я легко могу заблокироваться каждый раз....

Кроме того, мой обновленный код отражен выше. Я знаю, что мои вызовы событий Set & Reset должны находиться внутри вызовов критической секции.


person Eric    schedule 18.02.2011    source источник
comment
Оффтоп: Интересно, почему код не подсвечивается? Это происходит в режиме редактирования... Во всяком случае, иногда это происходит. знак равно   -  person Zsub    schedule 18.02.2011
comment
основные моменты для меня ... Может быть, попробовать очистить кеш браузера и перезапустить?   -  person Eric    schedule 18.02.2011
comment
Вы можете запустить это в отладчике, верно? Где потоки, когда он взаимоблокируется?   -  person Chris Becke    schedule 18.02.2011
comment
да, я могу запустить это в отладчике, и я также могу запустить его в моей основной кодовой базе, если я не столкнусь с какой-либо нагрузкой; то есть вызов метода TDGB::Print внутри цикла for или последовательно. Пожалуйста, посмотрите мои две тестовые функции (MyTest1 и MyTest2). В отладчике дохожу до EnterCriticalSection и зависаю.   -  person Eric    schedule 18.02.2011


Ответы (3)


Когда эти параметры закрыты, я бы задавал вопросы об этом объекте «Информация». Является ли это окном, родительским для которого является окно и в каком потоке оно было создано?

Если Info или его родительское окно было создано в другом потоке, может возникнуть следующая ситуация:

Консольный поток находится внутри критической секции, обрабатывая сообщение. Основной поток вызывает Print() и блокирует критический раздел, ожидая, пока консольный поток снимет блокировку. Консольный поток вызывает функцию для Info (Caption), в результате чего система отправляет сообщение (WM_SETTEXT) окну. SendMessage блокируется, поскольку целевой поток не находится в состоянии оповещения о сообщении (не блокируется при вызове GetMessage/WaitMessage/MsgWaitForMultipleObjects).

Теперь у вас тупик.

Такого рода #$(%^ может произойти всякий раз, когда вы смешиваете блокирующие подпрограммы со всем, что взаимодействует с окнами. Единственная подходящая блокирующая функция для использования в потоке графического интерфейса — это MSGWaitForMultipleObjects, в противном случае вызовы SendMessage для окон, размещенных в потоке, могут легко заблокироваться.

Избежать этого можно двумя способами:

  • Никогда не выполняйте взаимодействие с графическим интерфейсом в рабочих потоках. Используйте PostMessage только для отправки неблокирующих команд обновления пользовательского интерфейса в поток пользовательского интерфейса, ИЛИ
  • Используйте объекты событий ядра + MSGWaitForMultipleObjects (в потоке графического интерфейса), чтобы гарантировать, что даже когда вы блокируете ресурс, вы по-прежнему отправляете сообщения.
person Chris Becke    schedule 18.02.2011
comment
вы знаете, это, наверное, очень правильно! Я много сделал с многопоточностью, и у меня нет проблем где-либо еще; только здесь, где я звоню в пользовательский интерфейс Windows. Пользовательский интерфейс, кстати, является компонентами Embarcadero VCL. Я подозреваю, что основной поток заблокировал SendMessage, как вы предложили. Пошли тестировать это... - person Eric; 19.02.2011
comment
да, конечно, это было так. Я просто отключаю вызовы графического интерфейса и сбрасываю свой кольцевой буфер, или если я просто закомментирую все вызовы графического интерфейса в случае WAIT_OBJECT_0: и сбрасываю буфер по тайм-ауту, теперь все работает. Спасибо, что остался там, чтобы помочь выделить это, Крис! Меня весьма интересуют комментарии Бена Фойгта, которые, я думаю, я расследую. Я нашел демонстрацию проекта кода, которая обеспечивает эталонную реализацию. Временно я рассмотрю MSGWaitForMultipleObjects — новый вызов API для меня. - person Eric; 19.02.2011

Не зная, где он заходит в тупик, этот код трудно понять. Два комментария:

  • Учитывая, что это С++, вы должны использовать объект Auto для блокировки и разблокировки. На тот случай, если для Log когда-нибудь не станет катастрофическим выдавать исключение.

  • Вы сбрасываете событие в ответ на WAIT_TIMEOUT. Это оставляет небольшое окно возможности для второго вызова Print() для установки события, когда рабочий поток вернулся из WaitForMultiple, но до того, как он войдет в критическую секцию. Это приведет к сбросу события, когда на самом деле ожидаются данные.

Но вам нужно отладить его и показать, где он «тупиковый». Если один поток застрял на EnterCriticalSection, мы можем выяснить, почему. Если нет ни одного потока, то незавершенная печать является просто результатом потери события.

person Chris Becke    schedule 18.02.2011
comment
Эта информация помогла мне независимо от основной проблемы. Я обнаружил, что первопричина была в моем кольцевом буфере. В кольцевом буфере была ошибка, и он не возвращался, поэтому мертвая блокировка на самом деле не была мертвой блокировкой по определению. Мой кольцевой буфер просто не возвращался. Как только я исправил эту ошибку, все заработало. Я отмечаю этот ответ как правильный, поскольку он указывает на потенциальную проблему в случае WAIT_TIMEOUT. Спасибо! - person Eric; 18.02.2011
comment
просто заметка для тех, кто заинтересован. Мой кольцевой буфер получал данные так быстро, что пограничный случай, когда буфер обернулся, и мои указатели R/W (указатель чтения и записи отслеживают, где я нахожусь в кольце) внутри стали равными, и код кольцевого буфера при новой записи когда указатель R/W был равен, снова выглядел как полный буфер. Точно так же запись выглядела как пустой буфер. Таким образом, полное чтение или пустая запись вызвали тупиковую блокировку буфера в этом пограничном случае. После исправления мой тупик ушел. - person Eric; 18.02.2011
comment
Ах, дерьмо... после дальнейших тестов оказалось, что мой тупик не ушел. При сверхвысокой нагрузке я продолжаю зависать или, по крайней мере, мое приложение зависает, но это почти не используется, так как я исправил проблему с кольцевым буфером. - person Eric; 18.02.2011

Я бы настоятельно рекомендовал реализацию без блокировки.

Это не только позволит избежать потенциальной взаимоблокировки, но и инструментарий отладки — это то место, где вы абсолютно не хотите блокировать. Влияние форматирования отладочных сообщений на синхронизацию многопоточного приложения достаточно плохо... наличие блокировок, синхронизирующих ваш параллельный код только потому, что вы его инструментировали, делает отладку бесполезной.

Я предлагаю дизайн на основе SList (API Win32 предоставляет реализацию SList, но вы можете достаточно легко создать потокобезопасный шаблон, используя InterlockedCompareExchange и InterlockedExchange). Каждый поток будет иметь пул буферов. Каждый буфер будет отслеживать поток, из которого он поступил. После обработки буфера диспетчер журналов отправит буфер обратно в SList исходного потока для повторного использования. Потоки, желающие написать сообщение, отправят буфер в поток регистратора. Это также препятствует тому, чтобы какой-либо поток лишал другие потоки буферов. Событие для пробуждения потока регистратора при помещении буфера в очередь завершает проектирование.

person Ben Voigt    schedule 18.02.2011
comment
интересно!!! В свою защиту скажу, что эта консоль отладки обычно не активна. Для его включения требуется специальный переключатель командной строки, а затем только определенные вещи регистрируются через скрытое окно, используемое для выбора того, что следует регистрировать. Это система ведения журнала, которая вызывается только тогда, когда мне нужно распечатать из временного кода, включить код из скрытого выбора пользовательского интерфейса и т. Д. Но очень интересны ваши мысли в других областях, я использую критические разделы. Может быть, у вас есть хороший источник, чтобы порекомендовать прочитать? - person Eric; 19.02.2011
comment
Использование критических секций в других областях допустимо, если вы понимаете это. Но критические секции (или любые блокировки, в том числе запрятанные в распределителе или уровне ввода-вывода) в коде отладки — это катастрофа (программная версия принципа неопределенности Гейзенберга). Что касается ресурсов для получения дополнительной информации, я бы посоветовал вам просмотреть comp.programming.threads архивы. Я многому научился у некоторых из тех же экспертов до того, как Microsoft закрыла общедоступные группы новостей MS. - person Ben Voigt; 19.02.2011