線程自我終止會導致線程內部對象的析構異常?

一開始主線程A是作爲一個對話框CTestDlg存在,現在,在CTestDlg的成員函數OnStartThread中始一個新線程B,OnStartThread函數中CString局部變量strA(注1)作爲線程B工作函數的參數pParam,CTestDlg的成員函數OnEndThread用於設置中止線程B,見下面的示例代碼1

... 線程A(主線程)
CTestDlg::OnStartThread()
{
    CString strA = "the StrA will be leak!"; // strA作爲局部變量傳遞
    LPVOID pParam = (LPVOID)&strA;
    CWinThread* m_pThread = ::AfxBeginThread(&ThreadProc, pParam);
}

void CMsiTestDlg::OnEndThread()
{
     g_bStopThread = TRUE;
}

... 線程B
UINT ThreadProc(LPVOID pParam)
{
    CString strB = *(CString*)pParam; // 注意:堆數據的引用計數加1,這將可能產生內存泄漏!
    CString strC = "here's in thread B"; // 線程B工作函數的CString局部變量,這也可能產生內存泄漏! 

    while (!g_bStopThread)
    {
        if (g_bStopThread == TRUE)
        {
            AfxEndThread(0, TRUE);// 強行中止線程B,僅將線程中的棧數據清空,但不會對堆數據清空!
            // return 0; // 推薦使用!正常返回0, 使得strB能正常析構,將堆數據的引用計數減1,最終使得堆數據正常釋放。
        }

        Sleep(200); // 模擬線程工作
    }

    return 0; //正常返回0
}

在這裏,線程A與線程B的運行是非同步的,即線程A無需等待線程B的完成,啓動線程B後,先進入線程B的工作函數:

提示:如果對CString引用計數比較瞭解,應該知道:CString對象之間的複製是通過對堆數據的引用計數機制完成的。有關堆數據的數據結構如下:
struct CStringData
{
     long nRefs;             // reference count 引用計數
     int nDataLength;        // length of data (including terminator) 數據長度
     int nAllocLength;       // length of allocation 實際在堆中分配的長度
     // TCHAR data[nAllocLength]

     TCHAR* data()           // TCHAR* to managed data 數據的首指針
      { return (TCHAR*)(this+1); }
};


在本例中,strA作爲主線程A的一個函數中的局部變量,在初始化時,先在堆中分配數據,然後將堆數據的引用計數nRefs初始化爲1,當strA作爲參數傳至線程B的工作函數中,並賦值給strB,在這裏並非將strA中的堆數據在內存中重新複製一份給strB,而只是對原來堆數據的引用計數nRefs加1,最終,堆數據的引用計數nRefs爲2。

回到線程A,線程A將局部變量strA析構,在析構過程中先調用InterLockDecrement(參考2)函數將數據的引用計數nRefs減1並返回減1後的引用計數值,如果nRefs小於或等於0,則調用FreeData將堆數據清空。在這裏,堆數據的引用計數nRefs等於2,減1後nRefs等於1,因此,堆數據並未被釋放,這就產生了潛在的內存泄漏!

再回到線程B,如果線程B正常結束(即從工作函數中正常返回0),會將strB析構,根據上文可知此時數據引用計數nRefs等於1,再次調用InterLockDecrement將引用計數nRefs減1,nRefs等於0,因此,調用FreeData使堆數據清空,這將不存在內存泄漏;如果在線程B運行過程中,在主線程A中(CTestDlg)調用OnEndThread函數,並最終調用線程B中的AfxEndThread將其自身中止,接下來發生什麼呢?調試狀態下跟蹤至AfxEndThread內部:

void AFXAPI AfxEndThread(UINT nExitCode, BOOL bDelete)
{
#ifndef _MT
 nExitCode;
 bDelete;
#else
 // remove current CWinThread object from memory
 AFX_MODULE_THREAD_STATE* pState = AfxGetModuleThreadState();
 CWinThread* pThread = pState->m_pCurrentWinThread;
 if (pThread != NULL)
 {
  ASSERT_VALID(pThread);
  ASSERT(pThread != AfxGetApp());

  // cleanup OLE if required
  if (pThread->m_lpfnOleTermOrFreeLib != NULL)
   (*pThread->m_lpfnOleTermOrFreeLib)(TRUE, FALSE);

  if (bDelete)
   pThread->Delete();
  pState->m_pCurrentWinThread = NULL;
 }

 // allow cleanup of any thread local objects
 AfxTermThread();

 // allow C-runtime to cleanup, and exit the thread
 _endthreadex(nExitCode);
#endif //!_MT
}


AfxEndThread通過調用pThread->Delete()會使線程B的棧清空(開始線程B時就已爲線程B分配了棧:參考3),然後將線程B工作函數中已爲所有變量分配的棧全部清空,包括strB,但是這不會使strB得以正常析構,也就無法將堆數據的引用計數減1,並最終不會釋放堆數據,引發了內存泄漏!

針對此,我另做了一個測試,並證明了strB確實未被正常析構,見下面的示例代碼2:

... 線程A(主線程)
CTestDlg::OnStartThread()
{
    CString strA = "the StrA will be leak!"; // strA作爲局部變量傳遞
    LPVOID pParam = (LPVOID)&strA;
    CWinThread* m_pThread = ::AfxBeginThread(&ThreadProc, pParam);

    WaitForSingleObject(m_pThread->m_hThread, INFINITE);
}

void CMsiTestDlg::OnEndThread()
{
     g_bStopThread = TRUE;
}

... 線程B
UINT ThreadProc(LPVOID pParam)
{
    CString strB = *(CString*)pParam; // 注意:堆數據的引用計數加1,這將可能產生內存泄漏!
    CString strC = "here's in thread B"; // 線程B工作函數的CString局部變量,這也可能產生內存泄漏! 

    while (!g_bStopThread)
    {
        if (g_bStopThread == TRUE)
        {
            AfxEndThread(0, TRUE);// 強行中止自已,僅將線程中的棧數據清空,但不會對堆數據清空!
            // return 0; // 推薦使用!正常返回0, 使得strB能正常析構,將堆數據的引用計數減1,最終使得堆數據正常釋放。
        }


        g_bStopThread = TRUE; // 只進入一次, 下一次就終止自已

        Sleep(200); // 模擬線程工作
    }

    return 0; //正常返回0
}

比之示例1代碼,加了兩行代碼(粗體顯示)。
g_bStopThread = TRUE,使線程B立刻在下一次循環時結束;
WaitForSingleObject(m_pThread->m_hThread, INFINITE),等待線程B結束。
將斷點設置在OnStartThread末尾}號處,按F11進入strA的析構代碼:
CString::~CString()
//  free any attached data
{
     if (GetData() != _afxDataNil)
     {
          if (InterlockedDecrement(&GetData()->nRefs) <= 0)
               FreeData(GetData());
     }
}

剛進入時察看GetData()->nRefs值仍爲2(見上文,線程A中的strA和線程B中的strB兩次引用同一個堆數據),這意味着,在線程B自我終止後,並未正確析構strB,因而也未能對堆數據的引用計數減1。
執行完引用計數減1後,察看GetData()->nRefs值爲1,未進入FreeData(GetData())行以釋放堆數據,產生了內存泄漏!

至於爲什麼線程自我終止不能使線程內部對象正常析構,我需要進一步查閱相關資料,以後將會彌補。

其實,即使線程B不引用線程A中的堆數據仍然會發生問題,如果在線程B的工作函數中加入strC併爲其初始化賦值,調用AfxEndThread強行中止線程也會導致strC不能正常析構,產生內存匯漏!

總之,在線程內部調用AfxEndThread或TerminateThread以及其它有關線程終止的函數,藉以強行中止自已是不安全的,正確的做法是需要中止的時候,直接從工作線程函數中返回,以正確釋放堆棧!

注:
1. strA也可以設爲全局共享變量或靜態變量以作示例測試。

參考:

1. CSDN文檔:Win32 環境下的堆棧(一)

2. MSDN幫助

3. 關於1的一個不錯的評論:
  2003.11.9 14:21 Xleep 發表評論  我同意某位仁兄的看法,堆就是堆,棧就是棧。
國內很多寫書或譯書的把堆(heap)寫成堆棧,把棧(stack)也寫成堆棧。事實上兩者是不同的。
樓主寫的是棧(stack).棧就是線程一開始操作系統分配給這個線程的一塊內存(樓主已經講的很詳細了)。當你某個函數的局部變量很大比如接近1m,棧(stack)就會溢出。
堆正如mayax所說的那樣,分缺省堆和輔助堆,在C++,C中使用delete,new,malloc、free 來操作缺省堆(缺省大小爲1MB)。堆是不會溢出的。只有分配失敗。比如 pvoid pv = malloc(0x100000);當malloc看到缺省堆中沒有這麼大的內存就會返回null,這就是分配失敗。當然缺省堆不夠用了的話,在 win32中你可以用heapcreate來建立一個輔助堆,用heapalloc來分配堆內存,用heapfree來釋放堆內存,用 heapdestroy來銷燬這個輔助堆。
建議對這個東西還弄不懂的多看看Jeffrey Richter的《windows核心編程》,這裏面把win32的內存管理講的很清楚,不過這本書的中譯者還是把stack翻譯成堆棧,把heap也翻譯成堆棧。

另外值得一提的是全局變量不是放在堆裏面而是放在pe文件的數據節(section),當操作系統裝載程序的時候把pe文件的數據節映射到哪裏,全局變量就在那塊內存裏面。這個大家可以瞭解一下PE結構。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章