11/12/2013

多執行緒(multi-thread) - 發生死結(死鎖)(deadlock)時,該如何除錯?

相信用過多緒的人,一定對於發生死結後不知道卡在哪裡,無法除錯,感到很懊惱吧!
我有一個方式,有機會讓你抽絲剝繭的找出死結錯誤喔。

原理就是...先使用SetUnhandledExceptionFilter來設定當機例外處理,
之後創建一個定時執行緒,功用是定時檢查程式是否卡死,
若卡死,則故意讓程式當機,藉此觸發當機例外處理。
接著就能使用MiniDumpWriteDump來將所有記憶體都dump到檔案中。
最後就能用VC開啟此dump檔,慢慢的來觀察相關記憶體,找出死結所在。

使用場合:
1. client程式在玩家這邊執行時
2. server程式在機房執行時
就把dmp檔案拿回來後,趕快去熬夜修正死結吧!(悲慘ing)


〔1〕先將下述一大段程式碼,放置你的專案中

#define DUMP_TYPE (MINIDUMP_TYPE)(MiniDumpNormal | MiniDumpWithDataSegs | MiniDumpWithProcessThreadData | MiniDumpWithHandleData | MiniDumpWithUnloadedModules | MiniDumpScanMemory | MiniDumpWithIndirectlyReferencedMemory | MiniDumpWithFullMemory)

static void gDumpMiniDump( PEXCEPTION_POINTERS pExInfo )
{
    char buff_file_name[ MAX_PATH ] = {0};
    SYSTEMTIME time;
    ::GetLocalTime( &time );

#ifdef DEF_I_AM_CLIENT
    Tool::sSprintf( buff_file_name, MAX_PATH, "ClientDumpFile_%04d%02d%02d_%02d%02d%02d.dmp", time.wYear, time.wMonth, time.wDay, time.wHour, time.wMinute, time.wSecond );
#else
    Tool::sSprintf( buff_file_name, MAX_PATH, "ServerDumpFile_%04d%02d%02d_%02d%02d%02d.dmp", time.wYear, time.wMonth, time.wDay, time.wHour, time.wMinute, time.wSecond );
#endif

    HANDLE lhDumpFile = ::CreateFile( buff_file_name, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL ,NULL );
    if( lhDumpFile != INVALID_HANDLE_VALUE )
    {
        MINIDUMP_EXCEPTION_INFORMATION loExceptionInfo;
        loExceptionInfo.ExceptionPointers = pExInfo;
        loExceptionInfo.ThreadId = ::GetCurrentThreadId();
        loExceptionInfo.ClientPointers = TRUE;

        BOOL flag = ::MiniDumpWriteDump( ::GetCurrentProcess(),
                        ::GetCurrentProcessId(), lhDumpFile, DUMP_TYPE, &loExceptionInfo, NULL, NULL );
        if( !flag )
        {
        //    D_WARNING();
        }
        ::CloseHandle( lhDumpFile );
    }
    else
    {
    //    D_WARNING();
    }

//    GameEventInterface::sOnCrash();
}

static LONG WINAPI gCustomUnhandledExceptionFilter( PEXCEPTION_POINTERS pExInfo )
{
    gDumpMiniDump( pExInfo );
    return EXCEPTION_EXECUTE_HANDLER;
}

void gCreateDebugCrashDump()
{
    // 設定例外當機捕捉
    ::SetUnhandledExceptionFilter( gCustomUnhandledExceptionFilter );
}

〔2〕 然後,在程式一啟動時,立即呼叫gCreateDebugCrashDump();
作用是先設定好VC的當機例外處理。



〔3〕 創建一個定時執行緒(此是虛擬碼)
UINT WINAPI CasualServer::sCheckDeadlockThreadProc( LPVOID lpParam )
{
    CasualServer *pThis = (CasualServer*)lpParam;

    while( !gApp->IsShutdown() )
    {
        pThis->DebugSingleRun_Lock.Lock();
        BOOL flag = pThis->SendPocketBreak.DelayTimeFunc();
        pThis->DebugSingleRun_Lock.Unlock();
        if( flag )
        {
            Debug::SBox( "CasualServer::sCheckDeadlockThreadProc   timeout, prepare to dump file." );
            char *p = NULL;
            *p = 1; // 故意當機
        }
        ::Sleep( 50 );
    }
    return 0;
}

我的SendPocketBreak計時器是設定5分鐘會觸發,我會在主迴圈不斷的將計時器清為0,
當死結發生時,SendPocketBreak計時器就不會被清除。
(這個定時檢查的條件依據各自程式不同,而會改變,請自行決定定時的條件)
然後累積5分鐘到時,我就讓程式故意當機,就會觸發當機例外處理。
會產生一個蠻大的dmp檔案,產生的時間和大小依據使用的記憶體多寡而不同。
(約幾十MB到幾百MB)

有了dmp後,要如何使用呢?
把dmp搬移到你exe執行檔的位置,然後點擊dmp二下,會開啟VC,
然後選擇左方專案列表的功能,就能開始使用並定位到當機時的位置。
以我的話,就能靠gApp這個全域主體物件指標,來開始逐一檢查嚕。

PS:當你的程式發佈出去時,記得要將此專案的全部資料都備份起來(包含各種debug暫存檔),這樣到時dmp才能順利定位。
如果你exe重新編譯後,又使用舊版的dmp開啟的話,則無法定位喔!VC會找不到對應的位址。



thanks for reading. :D

沒有留言: