1. 程式人生 > >vector在多執行緒下的問題,迭代器失效造成程式崩潰。

vector在多執行緒下的問題,迭代器失效造成程式崩潰。

最近在做專案的過程中,遇到STL中vector的多執行緒訪問問題。問題大概是這樣的:有一個全域性的vector,一個寫程序對該vector進行插入操作(push_back()),同時有一個讀程序在監視該vector的內容並對其進行顯示(操作:size(), at(i)),沒有進行任何的執行緒同步,程式的編譯沒有任何問題,卻一直出現執行時錯誤,主要是陣列越界。 當時的考慮時:雖然vector不支援多執行緒,但是我的兩個執行緒,一個寫,一個卻是隻讀,按理說不應該有問題,大不了有髒資料,但是我不在乎。網上有很多人說是執行緒同步問題,但是個人感覺不是這個問題。後來看了一篇網文之後才晃然大悟,其實是vector的大小動態分配所造成的問題。簡單的說就是讀程序和寫程序在vector的記憶體動態重分配時記憶體地址不同步了(不知道說的對不對)。懶的再描述自己的具體問題了,直接把那篇網文貼上來,以供以後檢視。

原文內容:

程式出現了coredump,用gdb發現了出錯的地方。

vector<size_t> seq_offset;

......

if(seq_id < seq_offset.size())

{

seq_offset[seq_id]; //此處coredump

}

沒有超過vector的大小,訪問是不應該有問題的,而且這一現象是偶然出現,所以懷疑是多執行緒的原因。果然在另一個執行緒裡發現對seq_offset有 insert操作,insert本身不會影響訪問,但是當vector大小不夠的時候,insert會引發記憶體重新分配。STL程式碼(linux gcc-3.4.3 /usr/include/c++/3.4.3/bits)裡,vector的insert會呼叫_M_fill_insert,而當空間不夠時 _M_fill_insert會重新分配空間,並將相關資料結構指向新空間。檔案 vector.tcc

324 std::_Destroy(this->_M_impl._M_start, this->_M_impl._M_finish); //銷燬vector裡的元素,即呼叫各元素的解構函式

325 _M_deallocate(this->_M_impl._M_start, //收回存放各元素的空間,即free

326 this->_M_impl._M_end_of_storage - this->_M_impl._M_start);

327 this->_M_impl._M_start = __new_start.base(); //資料結構指向新的空間

328 this->_M_impl._M_finish = __new_finish.base();

329 this->_M_impl._M_end_of_storage = __new_start.base() + __len;

麻煩就在於當326行結束後,_M_start指向的是一塊已經釋放了的空間,這時候再有vector的訪問操作(如seq_offset[seq_id])當然就會coredump。其實STL完全可以先把_M_start賦給一個臨時變數,然後_M_start指向 新空間,最後再釋放臨時變數指向的空間,這樣可以保證在多執行緒下順利執行。不過STL文件說了——它不支援多執行緒——所以這樣做也沒錯。

加鎖也可以解決這個問題,不過那樣太低效了,不予考慮。最後的解決方案是,用vector的reserve方法預先分配好記憶體,免得在使用中動態增長。

///////////////////////////////////////////////////////////////////////////////////////////////

在STL的容器中,vector可以說是最容易理解和使用的容器了,以前使用陣列的時候,如果不確定有多少資料要儲存,就會預先分配一個大的陣列,如果實際沒有用到那麼多,又會浪費很多的記憶體資源,如果不分配大的陣列又擔心不夠用,有了vector之後,這些問題再也不用擔心了,vector會動態的增長空間,當vector空間不足時會自動申請一片更大的記憶體空間,以儲存新的資料。

vector動態記憶體增長的過程如下:

1) 申請一塊更大的記憶體空間以儲存新資料。

2) 將資料從舊記憶體空間拷貝到新記憶體空間。

3) 析構舊記憶體空間中的物件。

4) 釋放舊記憶體空間

對於程式設計師來說,這簡直就是一個神器,但是在享受vector帶來方便的同時,它也隱藏了

一些記憶體方面的問題,需要我們注意一下

1、預先分配兩倍的記憶體空間

對於一個數組,只要通過元素的個數以及元素的型別就可以得到佔用的記憶體空間,而對於

vector情況就不一樣,為了提高效率,vector會預先分配一定的記憶體空間,減少申請記憶體以及資料移動的開銷,因此vector實際佔用的記憶體空間要比需要的多一些。如果對記憶體要求比較高的程式,使用vector一定要小心了,當vector空間不足時,會申請一塊約是當前佔用記憶體兩倍的新空間以儲存更多的資料,若可用記憶體為1G,而當前vector已佔用的記憶體空間0.5G,當再插入一個元素,vector將會申請一塊約1G的記憶體空間,瞬間記憶體就被用光了,導致程式崩潰。每次擴容50%,94/2+94=141。

2、迭代器失效

當vector空間不足時,會申請一塊更大的記憶體空間,並將原指向記憶體空間的資料移動到新

的記憶體空間中,接著,舊的記憶體空間將會被回收。這裡就可能會出現問題了,當vector指向新的記憶體空間之後,原來指向舊記憶體空間的迭代器都會失效了,若再使用這些失效迭代器,將會出現coredump。因為當需要插入資料到vector的時候,就需要注意當前的迭代器還是否有效了。

3、釋放vector所佔記憶體

vector已經申請的記憶體空間並不會縮減,即使呼叫clear()函式,也只是清空vector的所有元素,真正佔用的記憶體空間也不會減少。有一種方法可以釋放vector佔用的記憶體空間,swap()

函式可以交換兩個vector物件所指向的記憶體空間,

下面例子說明了swap()函式的使用方法,假設擁有一個vector<int>物件vec:

vector<int> tmp; //tmp為空

vec.swap(tmp); //vec和tmp交換指向的記憶體空間。

如果程式碼想要更加精簡,則可以選擇下面的寫法:

vec.swap(vector<int>());//vec與臨時物件交換了指向的記憶體空間。

上面兩種方式都使用一個沒有資料的vector物件與當前vetor物件進行了交換,對於vec來講,其記憶體空間縮減為0,同理,若想vec變為指定大小,則只需要與一個大小一樣的vector物件進行swap即可。

4、預先申請記憶體空間

若一開始就大概確定需要的記憶體空間,則可以使用vector的reserve()函式預先申請足夠的記憶體空間,這樣做,既可以減少記憶體增長時的申請記憶體和資料移動的開銷,避免了迭代器失效的問題,也防止了申請了超出預期的的記憶體空間,導致記憶體耗盡的問題;若一開始不確定需要的記憶體空間,則可以先預留足夠大的空間,當所有資料都插入後,通過swap()函式去除多餘的容量即可。