靈巧指標與垃圾回收 (轉)

worldblog發表於2007-12-12
靈巧指標與垃圾回收 (轉)[@more@]

 
  在 和 中都有垃圾回收功能,員在分配一段後可以不再理會,而由垃圾回收自動回收,從而使程式設計師從複雜的記憶體管理中解脫出來。這是JAVA 和 C#的一大優點。而C++程式設計師在用 new 分配了一段記憶體後,還必須用 delete 釋放,否則將造成資源洩漏。因此,一些C++ 書上經常告誡程式設計師:要養成好的習慣,new 與 delete 要成對出現,時刻記住將記憶體釋放回。但是,事情只是這麼簡單嗎?
  經常地,在使用C++的過程中,我們會遇到下面的情形:
  class  A
  {
  public:
  A();
  ~A();
  SetNextPtr(A* Ptr)
  {pNext=Ptr;}
 
  private:
  A *  pNext;
  }
  一般地,為了不引起記憶體洩漏,我們會在析構中釋放pNext,象下面這樣:
  A::~A()
  {
  if(pNext)
  delete pNext;
  pNext=NULL;
  }
  對於一般情況,這樣就夠了,但在某些情形下,這樣也會出問題的,象下面這樣:
  A  *ptrB  =  new A;;
  A *ptrA = new A;
  ptrB->SetNextPtr(ptrA);
  ptrA->SetNextPtr(ptrB);
 
  delete  ptrB; 
  這樣會出問題,因為這些指標連成了一個迴環,無論從那一個點開始刪除,都會造成一個指標被刪除兩次以上,這將使得程式丟擲異常。當然,也有一些方法可以用來解決這個問題,但是我要說明的是:對於C++程式設計師來說,養成一個好的習慣並不難,難就難在有時候這樣將把你帶入一種邏輯的混亂當中 ,增加一些不必要的麻煩,有時甚至不知所措。
  可是如何解決這個問題呢?如果C++也具有垃圾回收的功能,那麼,這個問題當然就迎刃而解了。但是C++屬於編譯型語言,不會具備這個功能。長期以來,我也一直在思考這個問題,想找出一種方法來使自己從這種麻煩中解脫出來。直到最近開始學習泛型,看到靈巧指標的介紹以後,我靈光一閃,終於找到了辦法來解決這個問題。
  大家知道,靈巧指標具有一些靈巧特性,如在構造時可以自動初始化,析構時可以自動釋放所指的指標。我們就利用它的這些特性來實現我們的垃圾回收。
  首先,我們要想辦法來對我們用 new 分配的每一段記憶體增加引用記數,即記錄下當前指向它的靈巧指標個數,當最後一個指向它的指標被釋放時,我們就可以釋放這段記憶體了。由此,我們進行了new 和 delete 的全域性過載,並引入了CPtrManager 類。

void operator delete(void * p)
{
 
 int mark=thePtrManager.GetMarkFromPtr(p);
 if(mark>0)
  thePtrManager.UserDelete(mark);
 free(p);
}
void * operator new(size_t size)
{

 
  return thePtrManager.MallocPtr(size);

}
class CPtrManager 
{
public:
 int GetCount(int mark,void * p);  到當前的引用記數
 static CPtrManager* GetPtrManager(); 到全域性唯一的CPtrManager 指標
 void UserDelete(int mark);  除 mark 標誌的指標,並對指標和標誌復位
 void * MallocPtr(size_t size); ()它分配記憶體;
 BOOL AddCount(int mark,void * Ptr); 加引用記數
 BOOL Release(int mark,void * Ptr); 少引用記數
 int GetMarkFromPtr(void * Ptr);  過指標得到標誌
 CPtrManager();
 virtual ~CPtrManager();
private:
 static CPtrManager * p_this;  向全域性唯一的CPtrManager 指標
 void AddPtr(void * Ptr);  加一個新分配的記憶體
  CPtrArray  m_ptr;  放分配的指標的可變陣列
 CUIntArray m_count; 放指標的引用記數
 void*  pCurrent;  近剛分配的指標
 unsigned int m_mark; 近剛分配的指標的標誌
 CUIntArray m_removed;//存放m_ptr中指標被刪除後所空留的位置
};
  顧名思義,CPtrManager 就是用來管理指標的,對於我們用new 分配的每一個指標,都存放在m_ptr[index]中,並在m_count[index]中存放它的引用記數。同時,我們對每一個指標都增加了一個標誌(mark >0,<=0為無效),這個標誌同時存在於靈巧指標中(後面將看到),這是為了一種雙重保險,並且在這裡,這個標誌就等於指標在m_ptr中的,這也為查詢提供了方便。
  總的思路是這樣的:當我們用new分配一個指標時,這個指標將被存入CPtrManager中,當一個靈巧指標開始擁有這個指標時,CPtrManager將負責對這個指標的引用記數加 1 ,反之亦然,即一個靈巧指標開始釋放該指標的擁有權時,CPtrManager將負責對這個指標的引用記數減 1 ,如果引用記數為 0 ,那麼這個靈巧指標將負責對該指標 delete。
  下面是靈巧指標的部分介紹:
template
class auto_ptr
{
  public:
  auto_ptr()
  {mark=0;pointee=0;}
  auto_ptr(auto_ptr&rhs);
  auto_ptr(T*ptr);
  ~auto_ptr(){Remove();}
  T*operator->() const;
  operator T*();
  T&operator*()const;
  auto_ptr&operator=(auto_ptr&rhs);
  auto_ptr&operator=(T*ptr);
  private:
  void Remove(); 放所指指標的擁有權
  T*pointee;  擁有的指標
  int mark;//所擁有的指標的標誌
};
template void auto_ptr< T>::Remove()
{
  CPtrManager * pMana=CPtrManager::GetPtrManager();
 if(pointee&&pMana)
 {
  if(pMana->Release(mark,pointee))  少引用記數
  {
  if(pMana->GetCount(mark,pointee) ==0)
  delete pointee;  果引用記數為0,delete 指標
  } 
  else  擁有的指標不在CPtrManager 中,有可能在棧中
  {
  dec to do
  }


 }
 pointee=NULL;  位
 mark=0;
}
template auto_ptr< T>::auto_ptr(auto_ptr&rhs)
{
 pointee=rhs.pointee;
 mark=rhs.mark;
 CPtrManager * pMana=CPtrManager::GetPtrManager();
 if(pMana)
  pMana->AddCount(mark,pointee); 加引用記數
 
}
template auto_ptr< T>::auto_ptr(T*ptr)
{
  mark=0;
  pointee=ptr;
  CPtrManager * pMana=CPtrManager::GetPtrManager();
  if(pMana)
  {
  mark=pMana->GetMarkFromPtr(ptr); 到指標的標誌
  if(mark>0)
  pMana->AddCount(mark,pointee);  果標誌不為0,增加引用記數
  }
}
templateauto_ptr& auto_ptr< T>::operator=(auto_ptr&rhs)
{
 if(pointee!=rhs.pointee)
 {
  Remove();  放當前指標的擁有權
  pointee=rhs.pointee;
  mark=rhs.mark;
  CPtrManager * pMana=CPtrManager::GetPtrManager();
  if(pMana)
  pMana->AddCount(mark,pointee);

 }
 return *this;

}
template auto_ptr&auto_ptr< T>::operator = (T* ptr)
{
  if(pointee!=ptr)
  {
  Remove();
  pointee=ptr;
  CPtrManager * pMana=CPtrManager::GetPtrManager();
  if(pMana)
  {
  mark=pMana->GetMarkFromPtr(ptr);
  if(mark>0)
  pMana->AddCount(mark,pointee);
  }
  }

}
  當到了這裡時,我便以為大功告成,忍不住摸拳搽掌,很想試一試。結果發現對於一般的情況,效果確實不錯,達到了垃圾回收的效果。如下面的應用:
  auto_ptr p1=new  test;
  auto_ptrp2 = p1;
  auto_ptrp3 = new test;
  但是,很快地,我在測試前面提到的迴環時,就發現了問題,我是這樣測試的:
  class test
  {
 auto_ptr p;
  };

  auto_ptr p1=new test;
  auto_ptrp2 =new test;
  p1->p=p2;
  p2->p=p1;
  當程式離開作用域後,這兩塊記憶體並沒有象我想象的那樣被釋放,而是一直保留在堆中,直到程式結束。我仔細分析造成這種現象的原因,發現了一個非常有趣的問題,我把它稱之為互鎖現象。
  上面p1 所擁有的指標被兩個靈巧指標所擁有,除p1外,還有p2所擁有的 test 類中的靈巧指標p,p2亦然。也就是說,這兩塊記憶體的指標的引用記數都為 2 。當程式執行離開作用域後,p1,p2被析構,使它們的引用記數都為1,此後再沒有靈巧指標被析構而使它們的引用記數變為 0 ,因此它們將長期保留在堆中。這就象兩個被鎖住的箱子,其中每個箱子中都裝著對方的鑰匙,但卻無法把彼此開啟,這就是互鎖現象。
  可是如何解決呢?看來必須對它進行改進。同時,我也發現上面的方法不支援多執行緒。所以,我們改進後的方法不僅要解決互鎖現象,而且還要支援多執行緒。下面是我改進後的方法:
  首先是如何發現這種互鎖現象。我們知道,互鎖現象產生的根源在於擁有堆中記憶體的靈巧指標本身也存在於已分配的堆記憶體中,那麼,如何發現靈巧指標是存在於堆中還是棧中就成了問題的關鍵。由此,我引入了一個新的類 CPtr,由它來管理用 new 分配的指標,而 CPtrManager 專門用來管理 CPtr。如下所示:
class CPtr
{
 friend class CMark;
public:
 int GetPtrSize();  到用 new 分配指標的記憶體的大小
 CMutex * GetCMutex(); 於執行緒同步
 void * GetPtr(); 到用 new 分配的指標
 CPtr();
 virtual ~CPtr();
 int GetCount(); 到引用記數
  void AddAutoPtr(void * autoPtr,int AutoMark);//加入一個擁有該指標的靈巧指標
 BOOL DeleteAutoPtr(void * autoPtr); 放一個靈巧指標的擁有權
 void SetPtr(void * thePtr,int Size,  int mark,int count=0);  置一個新的指標
 
  void operator delete(void * p)
 {
  free(p);
 }
  void * operator new(size_t size)
 { 
  return malloc(size);
 }

private:
 int m_mark;  針標誌
 int m_count; 用記數
 void * m_ptr; 配的指標
 int m_size; 針指向記憶體的大小
 CPtrArray AutoPtrArray; 放擁有該指標的所有靈巧指標的指標陣列
 CUIntArray m_AutoMark; 巧指標標誌:0 in the stack; >0 =mark
 CMutex mutex; 於執行緒同步
};
class CPtrManager 
{
public:
 int GetAutoMark(void * ptr); 過靈巧指標的指標,得到靈巧指標標誌
 CPtrManager();
 virtual ~CPtrManager();
 int GetMarkFromPtr(void * Ptr);
 void *MallocPtr(size_t size);
  BOOL bCanWrite();
 void DeletePtr(int mark,void * Ptr);
 void AddPtr(void *Ptr,int PtrSize);
 static CPtrManager * GetPtrManager();
 CPtr * GetCPtr(void * Ptr,int mark);//透過指標和指標標誌得到存放該指標的 CPtr

private:
  CPtrArray m_ptr; 放 CPtr 的指標陣列
 void*  pCurrent;
 unsigned int m_mark;
 CUIntArray m_removed;
 BOOL bWrite; 解決互鎖現象的過程中,謝絕其它執行緒的處理
 static CPtrManager * p_this;
 CMutex mutex;//用於執行緒同步
 CMarkTable myMarkTable;
 void RemoveLockRes(); 理互鎖記憶體
 void CopyAllMark(); 理互鎖現象前,先對所有的CPtr進行複製
 static UINT myThreadProc(LPVOID lparm); 理互鎖現象的執行緒函式
 CWinThread* myThread;
 void BeginThread()
 { myThread=AfxBeginThread(myThreadProc,this,THREAD_PRIORITY_ABOVE_NORMAL);}
};
  上面的應用中加入了靈巧指標的標誌,其實,這個標誌就等於該靈巧指標所存在的記憶體的指標的標誌。例如:我們用 new 分配了一個 test 指標,假如這個指標的標誌 mark=1,那麼,這個 test 中的靈巧指標 auto_ptr p 的標誌 automark=1。如果一個靈巧指標存在於棧中,那麼它的 automark=0。反之亦然,如果一個靈巧指標的 automark 等於一個指標的 mark,那麼,該靈巧指標必存在於這個指標所指的記憶體中。可是,如何得到這個標誌呢,請看下面這個函式的實現:
int CPtrManager::GetAutoMark(void *ptr)
{
  CSingleLock singleLock(&mutex);
  singleLock.Lock(); 程同步

  int size =m_ptr.GetSize();
  for(int i=1;i  {
  CPtr* theCPtr=(CPtr*)m_ptr[i];
  if(theCPtr)
  {
  int ptrFirst=(int)theCPtr->GetPtr();//得到記憶體的首指標
  int ptrEnd=ptrFirst+theCPtr->GetPtrSize();//得到記憶體的尾指標
  int p=(int)ptr; 巧指標的指標
  if(p>=ptrFirst&&p<=ptrEnd)//比較靈巧指標的指標是否在首尾之間
  return  i;
  }
  }
  return 0;
}
  這個函式的原理就在於:如果一個靈巧指標存在於一塊記憶體中,那麼該靈巧指標的指標必在這塊記憶體的首尾指標之間。
  解決了靈巧指標的位置問題,下一步就是要找出所有被互鎖的記憶體的指標。這個好實現,只要所有擁有這個指標的靈巧指標的 automark > 0 ,那麼,這塊記憶體就可能被互鎖了(注意只是可能),接著看下面的實現:
class CMark
{
 friend class CMarkTable;
public:
 CMark(){}
 virtual ~CMark(){}


  void operator delete(void * p)
 {
  free(p);
 }
  void * operator new(size_t size)
 { 
  return malloc(size);
 }
 void CopyFromCPtr(CPtr* theCPtr); CPtr 中複製相關資訊
 BOOL bIsNoneInStack(); 斷擁有該指標的所有靈巧指標是否都不在棧中
 void Release(); 除該指標的互鎖
private:
 int m_mark; 針的標誌
 CPtrArray autoptrArray; 有該指標的所有靈巧指標的指標陣列
 CUIntArray automarkArray;//擁有該指標的所有靈巧指標的標誌

};
class CMarkTable
{
public:
 CMarkTable(){Init();}
 virtual ~CMarkTable(){}
 
 void AddCMark(CMark * theCMark);
 BOOL FindMark(int mark);
 void Init();
 void DoLockMark(); 理互鎖問題
private:
 CPtrArray CMarkArray; 存從CPtrManager 中複製過來的指標資訊的 CMark 指標陣列
 CPtrArray CLockMarkArray; 放互鎖的記憶體
 void GetLockMark(); 到所有可能被互鎖的記憶體的 CMark,結果存放於CLockMarkArray
 BOOL FindLockMark(int mark); 斷一個靈巧指標是否存在於CLockMarkArray所包含的指標中
 void RemoveUnlockMark();//去除假的互鎖記憶體
 void RemoveGroup(int automark);//對互相有聯絡的相互死鎖的記憶體進行分組

};
  這裡又引入了兩個類:CMark 和 CMarkTable ,這是為了在處理互鎖問題之前,對 CPtrManager 中的 CPtr 進行快速複製,以防止影響其它執行緒的正常執行。其實,這裡的 CMark 與 CPtr 沒有什麼區別,它只是簡單地從 CPtr 中複製資訊,也就是說,它等同於 CPtr 。
  為了處理互鎖問題,先要把可能被互鎖的記憶體指標找出來,看下面函式的實現:
void CMarkTable::GetLockMark()
{
 CLockMarkArray.SetSize(0);
 int size=CMarkArray.GetSize();
 for(int i=0;i {
 CMark * theMark=(CMark*)CMarkArray[i];
 if(theMark)
 {
  if(theMark->bIsNoneInStack())
  CLockMarkArray.SetAtGrow(i,theMark);
 }
 }
}
  把這些記憶體找出來之後,就需要把那些假鎖的記憶體找出來,什麼是假鎖呢?看下面的例子:
  對於指標 ptrA ,如果它的靈巧指標 autoA 存在於指標 ptrB 中,而 ptrB 的靈巧指標 autoB 又存在於 ptrA 中,那麼 ptrA 和 ptrB 是真鎖,但是如果ptrB 的靈巧指標 autoB 存在於指標 ptrC 中,而 ptrC的靈巧指標 autoC 存在於棧中,那麼, ptrA 和 ptrB 屬於假鎖。怎麼找出假鎖的記憶體呢?看下面函式的實現:
void CMarkTable::RemoveUnlockMark()
{
CUIntArray UnlockMarkArray;
BOOL bNoneRemoveed;
do
{
 bNoneRemoveed=TRUE;
 UnlockMarkArray.SetSize(0);
 int size=CLockMarkArray.GetSize();
 for(int i=0;i {
  CMark * theMark=(CMark*)CLockMarkArray[i];
  if(theMark)
  {
  int size1=(theMark->automarkArray).GetSize();
  for(int j=0;j  {
  int mark=(theMark->automarkArray)[j];
  if(!FindLockMark(mark)) 斷靈巧指標是否存在於CLockMarkArray所包含的指標中
  {
 UnlockMarkArray.InsertAt(0,i); to remove
 bNoneRemoveed=FALSE;
 break;
  }
  }
  }
  else
  {  UnlockMarkArray.InsertAt(0,i);
  bNoneRemoveed=FALSE;
  }
 }
 int size2=UnlockMarkArray.GetSize();
 for(int k=0;k {
  int m=UnlockMarkArray[k];
  CLockMarkArray.RemoveAt(m);
 }
}while(!bNoneRemoveed);

}
  上面函式的原理就是:不停地刪除那些靈巧指標不在CLockMarkArray所包含的指標中的指標,直到所有的指標的靈巧指標都存在於CLockMarkArray所包含的指標中。
  所有被互鎖的記憶體被找出來了,那麼,下一步就是如何解鎖的問題了。由此,我對靈巧指標引入了一個父類parent_autoptr 如下:
class parent_autoptr
{
public:
 parent_autoptr()
 {thisAutoMark=0;}
 virtual ~parent_autoptr(){}
 virtual void Release(){} 放指標的擁有權
protected:
 int thisAutoMark; 放靈巧指標標誌
};
  在靈巧指標中,對函式 Release() 進行了過載。
template
class auto_ptr :public parent_autoptr
{
  public:
  virtual void Release(){Remove();}
  auto_ptr()
  {mark=0;pointee=0;thisAutoMark=GetAutoMark();}
  auto_ptr(auto_ptr&rhs);
  auto_ptr(T*ptr);
  ~auto_ptr(){Remove();}
  T*operator->() const;
  operator T*();
  T&operator*()const;
  auto_ptr&operator=(auto_ptr&rhs);
  auto_ptr&operator=(T*ptr);
  private:
  void Remove();
  int GetAutoMark();
  CMutex *GetCMutex();
  void ReadyWrite();
  T*pointee;
  int mark;
 
};
  在 CMarkTable 和 CMark 中對互鎖記憶體進行了釋放,如下:
void CMarkTable::DoLockMark()
{
GetLockMark();
RemoveUnlockMark();

int size=CLockMarkArray.GetSize();
while(size)
{
 CMark* theMark=(CMark*)CLockMarkArray[0];
 CLockMarkArray.RemoveAt(0);
 if(theMark)
 {
  int size2=(theMark->automarkArray).GetSize();
  for(int i=0;i  {
  int automark=(theMark->automarkArray)[i];
  RemoveGroup(automark);
  }
  theMark->Release();
 
 }
 size=CLockMarkArray.GetSize();
}

Init();
}

void CMark::Release()
{
  int size=autoptrArray.GetSize();
  for(int i=0;i  {
 parent_autoptr * thePtr=(parent_autoptr *)autoptrArray[i];
 thePtr->Release();
  }
}

  到了現在,終於算是大功告成了,我馬上把它投入測試當中,發現工作得非常好,即使開闢20至30個執行緒,程式也工作得很好,並沒有丟擲異常,而且垃圾回收的功能也非常好。但是,如果執行緒太多,那麼在 CPtrManager 中為了保證執行緒同步,將會造成瓶頸效應,嚴重者將會嚴重影響執行。同時,如果每個執行緒都不停地產生死鎖記憶體,那麼,垃圾回收將應接不暇,時間長了,也會造成系統的資源耗盡。
  程式碼的使用很簡單,你只需要將我所附的兩個加入到工程中,然後,在你的 C*App 中加入如下一段程式碼就行了:
   CPtrManager thePtrManager;
  這將保證 thePtrManager 在程式最後結束的時候才被析構。
  如果你是在一個新的工程中使用,這就夠了,但是,如果你還要使用原來的程式碼,特別是有指標引數的傳遞時,那麼,你必須注意了。
  如果需要從老程式碼中接收一個指標,而且這個指標需要你自己釋放,那麼可以使用靈巧指標,如果不需要釋放,那麼只能使用一般指標;
  如果需要傳遞一個指標給老程式碼,而且這個指標需要你自己釋放,那麼可以使用靈巧指標,否則,只能使用一般指標。
 
  我將隨後附上所有,由於沒有經過嚴格測試,所以大家在使用前最好再測試一遍,最好能將發現的問題公佈或寫信告之於我,本人表示感謝。
  歡迎大家測試,批評指正和改進,
  E:  ">ydshzhy@263.net


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-991939/,如需轉載,請註明出處,否則將追究法律責任。

相關文章