C++ vector在多線程操作中出現(xiàn)內(nèi)存錯誤問題及解決
vector在多線程操作中出現(xiàn)內(nèi)存錯誤問題
C++ vector的reserve和resize詳解
reserve是容器預(yù)留空間,但在空間內(nèi)不真正創(chuàng)建元素對象,所以在沒有添加新的對象之前,不能引用容器內(nèi)的元素。加入新的元素時,要調(diào)用push_back()/insert()函數(shù)。
resize是改變?nèi)萜鞯拇笮?,且在?chuàng)建對象,因此,調(diào)用這個函數(shù)之后,就可以引用容器內(nèi)的對象了,因此當(dāng)加入新的元素時,用operator[]操作符,或者用迭代器來引用元素對象。此時再調(diào)用push_back()函數(shù),是加在這個新的空間后面的。
vector在多線程中操作舉例:
有一個全局變量 vector goods_list;
在A線程中從服務(wù)器獲取最新商品列表,goods_list.push_back()
在B線程中不斷的下載商品圖片,
Goods &goods = goods_list.at(i)
讀取goods .pic_url,下載完成后賦值 goods.local_pic = local_pic
以上簡單的邏輯,缺導(dǎo)致程序崩潰,提示內(nèi)存寫入錯誤。
調(diào)試定位到goods.local_pic = local_pic這一句。
估計就是多線程的問題。
查了一下資料,原來vector每次push_back都會重新分配內(nèi)存,導(dǎo)致goods 這個引用無效,所以goods.local_pic = local_pic賦值寫入的時候就會寫入到一個無效的地址,導(dǎo)致程序崩潰。
解決辦法
加鎖也可以解決這個問題,不過那樣太低效了,不予考慮。最后的解決方案是,用vector的reserve方法預(yù)先分配好內(nèi)存,免得在使用中動態(tài)增長。
在構(gòu)造函數(shù)中提前對goods_list.reserve(30000)分配足夠的固定內(nèi)存,這樣就不用每次pushback都申請增加內(nèi)存、重新分配內(nèi)存 導(dǎo)致的原內(nèi)存地址無效,而且效率也高很多。
跨平臺使用C++ vector的多線程問題
源起
最近碰到一個linux下程序崩潰的問題,涉及到vector的多線程使用的問題。由于是第二次折騰這個問題,所以把過程記錄下來。
簡單介紹一下背景:程序為windows和linux跨平臺使用。使用一套代碼,會分別編譯兩個平臺下的不同版本。
程序涉及的結(jié)構(gòu)。使用了一個全局變量的vector來保存數(shù)據(jù),有兩個線程,一個線程是周期執(zhí)行的,每個周期開始時檢查全局變量中是否有數(shù)據(jù),如果有就取出來處理,然后清空。另一個線程等待外部輸入數(shù)據(jù),如果有數(shù)據(jù)就放進vector。
示例代碼如下:
#include "stdafx.h" #include <vector> typedef void *(TASK_ENTRY_POINT)(void *); const unsigned short ? Task_Priority_Base?? ?= 50;?? ??? ?// 基本優(yōu)先級 #if defined WIN32?? ??? ?// windows 操作系統(tǒng) ?? ?typedef void * SIGNAL_HANDLE; #else?? ??? ??? ??? ??? ?// 標(biāo)準(zhǔn)linux 操作系統(tǒng) ?? ?#include <semaphore.h>?? ? ?? ?typedef sem_t * SIGNAL_HANDLE; #endif ?? ?void SleepUs(unsigned long us) ?? ?{ #if defined WIN32?? ??? ??? ??? ?// windows 操作系統(tǒng) ?? ??? ?::Sleep(us); #else?? ??? ??? ??? ??? ??? ??? ?// 標(biāo)準(zhǔn)linux 操作系統(tǒng) ?? ??? ?if (us>60000)?? ??? ??? ??? ?// > 1min ?? ??? ??? ?sleep(us/1000000); ?? ??? ?else ?? ??? ??? ?usleep(us); #endif ?? ?} void SleepMs(unsigned long Ms) { #if defined WIN32?? ??? ??? ??? ?// windows 操作系統(tǒng) ?? ??? ?::Sleep(Ms); #else?? ??? ??? ??? ??? ??? ??? ?// 標(biāo)準(zhǔn)linux 操作系統(tǒng) ?? ?if (Ms>60000)?? ??? ??? ??? ?// > 1min ?? ??? ?sleep(Ms/1000); ?? ?else ?? ??? ?usleep(Ms*1000); #endif } static int running_tasks = 0; void CreateTask(TASK_ENTRY_POINT EntryFunc,int Priority=0, void *ArgP = NULL); typedef std::vector<int >?? ?vectorType; vectorType testvec; bool init_task_library() { ?? ?running_tasks = 0; ?? ?int St=0; ?? ?// 設(shè)置調(diào)度策略或基本優(yōu)先級 #if defined WIN32?? ??? ??? ??? ?// windows 操作系統(tǒng) ?? ?SetPriorityClass(GetCurrentProcess(),HIGH_PRIORITY_CLASS); #else?? ??? ??? ??? ??? ??? ??? ?// 標(biāo)準(zhǔn)linux 操作系統(tǒng) ?? ?sched_param?? ?Param; ?? ?int?? ??? ??? ?PriorityMax,PriorityMin; ?? ?// 禁止內(nèi)存交換 //?? ?St=mlockall(MCL_CURRENT|MCL_FUTURE); ?? ?// 設(shè)置優(yōu)先級 ?? ?PriorityMax?? ?= sched_get_priority_max(SCHED_RR); ?? ?PriorityMin?? ?= sched_get_priority_min(SCHED_RR); ?? ?if (Task_Priority_Base>PriorityMax) ?? ??? ?Param.__sched_priority = PriorityMax; ?? ?else ?? ??? ?Param.__sched_priority = Task_Priority_Base; ?? ?St=sched_setscheduler(0,SCHED_RR,&Param); #endif ?? ?return true; } void CreateTask(TASK_ENTRY_POINT EntryFunc,int Priority, void *ArgP) { ?? ?static bool LibInit=false; ?? ?if (!LibInit) ?? ??? ?LibInit=init_task_library(); ?? ?assert(LibInit); ?? ?assert(EntryFunc); #if defined WIN32?? ??? ??? ??? ?// windows 操作系統(tǒng) ?? ?HANDLE Tid; ?? ?Tid=(HANDLE)::_beginthread((void (*)(void *))EntryFunc, 0, ArgP); ?? ?::SetThreadPriority(Tid,Priority); #else?? ??? ??? ??? ??? ??? ??? ?// 標(biāo)準(zhǔn)linux 操作系統(tǒng) ?? ?int St; ?? ?pthread_t ?? ??? ?Tid; ?? ?sched_param?? ??? ?Param; ?? ?int?? ??? ??? ??? ?Policy; ?? ?// 創(chuàng)建線程 ?? ?St = pthread_create(&Tid,NULL,EntryFunc,ArgP); ?? ?assert(St==0); ?? ?pthread_detach(Tid); ?? ?// 設(shè)置線程參數(shù) ?? ?Policy = SCHED_RR; ?? ?Param.__sched_priority=Task_Priority_Base + Priority; ?? ?St=pthread_setschedparam(Tid,Policy,&Param); #endif ?? ?++running_tasks; } void *addElement(void *ArgP) { ?? ?int i=0; ?? ?for(int i=0;i<10000;i++) ?? ?{ ?? ??? ?int res=i; ?? ??? ?for(int j=0;j<1000;j++) ?? ??? ?{ ?? ??? ??? ?testvec.push_back(res); ?? ??? ?} ?? ??? ?printf("addelement %d \n",res); ?? ??? ?SleepMs(100); ?? ?} ?? ?printf("addelement done\n"); ?? ?return NULL; } void *clearElement(void *ArgP) { ?? ?int i=0; ?? ?for(int i=0;i<100000000;i++) ?? ?{ ?? ??? ?if(testvec.size()>0) ?? ??? ?{ ?? ??? ??? ?for(vectorType::iterator ite=testvec.begin(); ite!=testvec.end(); ++ite)//崩點1 ?? ??? ??? ?{ ?? ??? ??? ??? ?{ ?? ??? ??? ??? ??? ?printf("get %d size=%d begin\n",(*ite),testvec.size());//,&*(testvec.begin()));//崩點2 ?? ??? ??? ??? ?} ?? ??? ??? ?} ?? ??? ??? ?printf("clear\n"); ?? ??? ??? ?testvec.clear();//崩點3 ?? ??? ?} ?? ??? ?SleepMs(1); ?? ?} ?? ?printf("clearelement done\n"); ?? ?return NULL; } int main(int argc, char* argv[]) { ?? ?CreateTask(addElement); ?? ?CreateTask(clearElement); ?? ?printf("done\n"); ?? ?while(1) ?? ?{ ?? ??? ?SleepMs(10000); ?? ?} ?? ?return 0; }
reserve問題
真實代碼中周期執(zhí)行的線程大部分時間在sleep,而接收數(shù)據(jù)的線程也在很少的情況下才會收到數(shù)據(jù),因此運行了很長時間也沒有出現(xiàn)問題。但是示例代碼中,不管是linux還是windows下,卻是一跑就崩的。在window下報“vector iterators incompatible” ,在linux下直接segfault。
首先說的是reserve問題。
vector的內(nèi)存是動態(tài)分配的,因此只要vector的大小超過了當(dāng)前的大小,就會重新開辟一塊新的內(nèi)存,大小為現(xiàn)在大小的兩倍,這就導(dǎo)致了如果大小漲了的話,內(nèi)存會變化的。而如果一個線程里在一直寫,另一個線程里用iterator來讀,那么如果第一個線程里內(nèi)存已經(jīng)變了,讀的線程還用原來的地址,就會導(dǎo)致程序崩潰。
這個問題還是比較好解決的。就是首先為vector保留內(nèi)存大小。使用reserve函數(shù)
對代碼的改進:
main函數(shù)改為:
int main(int argc, char* argv[]) { ?? ?char name[1024]; ?? ?sprintf(name, "testSyncMutex"); ?? ?testMutex = CreateTrigger(name); ?? ?FireTrigger(testMutex); ?? ?testvec.reserve(10000000); ?? ?printf("hello world\n"); ?? ?CreateTask(addElement); ?? ?CreateTask(clearElement); ?? ?printf("done\n"); ?? ?while(1) ?? ?{ ?? ??? ?SleepMs(10000); ?? ?} ?? ?return 0; }
clear問題
經(jīng)過了reserve的修改,生產(chǎn)環(huán)境的代碼大概率不會崩了,但是小概率事件在大基數(shù)面前也會出現(xiàn)。程序還是崩了。再次檢視了生產(chǎn)代碼,覺得應(yīng)該加個鎖了。
vector并不是線程安全的,所以,雖然生產(chǎn)環(huán)境下概率比較小,但是仍然是存在漏洞的。就比如示例代碼,仍然會崩。
一個線程里寫,另一個線程里讀,而且可能崩在任何讀的地方(代碼注釋崩點1~崩點3)。
那么下面就是怎么改了,通過信號量來加鎖。增加函數(shù)
SIGNAL_HANDLE?? ?testMutex; // 創(chuàng)建信號燈 SIGNAL_HANDLE?? ?CreateTrigger(const char* SigName) { ?? ?assert(SigName); #if defined WIN32?? ??? ??? ??? ?// windows 操作系統(tǒng) ?? ?return ::CreateSemaphoreA(NULL,0,1,SigName); ?? ?/*return ::CreateEvent(?? ?NULL, ? ? ? // no security attributes ?? ??? ??? ??? ??? ??? ??? ?FALSE,?? ??? ?// auto reset ?? ??? ??? ??? ??? ??? ??? ?FALSE, ? ? ?// initially not signaled ?? ??? ??? ??? ??? ??? ??? ?SigName);?? ?// name of mutex ?? ?*/ #else?? ??? ??? ??? ??? ??? ??? ??? ??? ?// 標(biāo)準(zhǔn)linux 操作系統(tǒng) ?? ?sem_t * SemP; ?? ?SemP=new sem_t; ?? ?assert(SemP); ?? ?int St=sem_init(SemP,0,0);?? ??? ??? ?// 線程間共享,初值為0 ?? ?assert(St != -1); ?? ?return SemP; //?? ?return sem_open(SigName,O_CREAT, S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH,0); #endif } // 釋放信號燈 void?? ?FreeTrigger(SIGNAL_HANDLE Handle) { ?? ?assert(Handle); #if defined WIN32?? ??? ??? ??? ??? ??? ?// windows 操作系統(tǒng) ?? ?::CloseHandle(Handle); #else?? ??? ??? ??? ??? ??? ??? ??? ??? ?// 標(biāo)準(zhǔn)linux 操作系統(tǒng) ?? ?sem_destroy(Handle); ?? ?delete Handle; ?? ?//sem_close(Handle); #endif } // 觸發(fā)信號燈 void?? ?FireTrigger(SIGNAL_HANDLE Handle) { ?? ?assert(Handle); #if defined WIN32?? ??? ??? ??? ?// windows 操作系統(tǒng) ?? ?//::SetEvent(Handle); ?? ?ReleaseSemaphore (Handle,1,NULL); #else?? ??? ??? ??? ??? ??? ??? ?// 標(biāo)準(zhǔn)linux 操作系統(tǒng) ?? ?sem_post(Handle); #endif } // 等待信號燈 void WaitTrigger(SIGNAL_HANDLE Handle) { ?? ?assert(Handle); #if defined WIN32?? ??? ??? ??? ?// windows 操作系統(tǒng) ?? ?::WaitForSingleObject(Handle, INFINITE); #else?? ??? ??? ??? ??? ??? ??? ?// 標(biāo)準(zhǔn)linux 操作系統(tǒng) ?? ?sem_wait(Handle); #endif }
main函數(shù)改為:
int main(int argc, char* argv[]) { ?? ?char name[1024]; ?? ?sprintf(name, "testSyncMutex"); ?? ?testMutex = CreateTrigger(name); ?? ?FireTrigger(testMutex); ?? ?testvec.reserve(10000000); ?? ?CreateTask(addElement); ?? ?CreateTask(clearElement); ?? ?while(1) ?? ?{ ?? ??? ?SleepMs(10000); ?? ?} ?? ?return 0; }
addElement函數(shù)改為
void *addElement(void *ArgP) { ?? ?int i=0; ?? ?int res=0; ?? ?for(int i=0;i<10000;i++) ?? ?{ ?? ??? ?for(int j=0;j<1000;j++) ?? ??? ?{ ?? ??? ??? ?WaitTrigger(testMutex); ?? ??? ??? ?res=i*1000+j; ?? ??? ??? ?testvec.push_back(res); ?? ??? ??? ?FireTrigger(testMutex); ?? ??? ?} ?? ??? ?SleepMs(100); ?? ?} ?? ?return NULL; }
clearElement中有兩種改法,一種是對整個循環(huán)加鎖。
代碼如下:
void *clearElement(void *ArgP) { ?? ?int i=0; ?? ?for(int i=0;i<100000000;i++) ?? ?{ ?? ??? ?i++; ?? ??? ?if(testvec.size()>0) ?? ??? ?{ ?? ??? ?WaitTrigger(testMutex); ?? ??? ??? ?for(vectorType::iterator ite=testvec.begin(); ite!=testvec.end(); ++ite) ?? ??? ??? ?{ ?? ??? ??? ??? ??? ?printf("get %d size=%d it%x\n",(*ite),testvec.size(),&*ite); ?? ??? ??? ?} ?? ??? ??? ?testvec.clear(); ?? ??? ? ? ?FireTrigger(testMutex); ?? ??? ?} ?? ??? ?SleepMs(1); ?? ?} ?? ?return NULL; }
但是這種做法的副作用也很明顯,如果讀的線程中執(zhí)行的操作較多或需要執(zhí)行的數(shù)據(jù)條數(shù)較多,可能會占用寫線程中的執(zhí)行時間。
另一種改法是加鎖的位置更加分散,如下:
void *clearElement(void *ArgP) { ?? ?int i=0; ?? ?for(int i=0;i<100000000;i++) ?? ?{ ?? ??? ?if(testvec.size()>0) ?? ??? ?{ ?? ??? ? WaitTrigger(testMutex); ?? ??? ??? ?for(vectorType::iterator ite=testvec.begin(); ite!=testvec.end(); WaitTrigger(testMutex),++ite) ?? ??? ??? ?{ ?? ??? ??? ??? ??? ?printf("get %d size=%d it%x\n",(*ite),testvec.size(),&*ite); ?? ??? ??? ??? ??? ?FireTrigger(testMutex); ?? ??? ??? ?} ?? ??? ??? ?testvec.clear(); ?? ??? ??? ?FireTrigger(testMutex); ?? ??? ?} ?? ??? ?SleepMs(1); ?? ?} ?? ?return NULL; }
注意
最后用這種方法修復(fù)了錯誤。一個感受就是,墨菲定律。程序中可能出錯的地方,一定會出錯。所以在用到非線程安全的容器時一定要注意加保護。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
C語言數(shù)據(jù)結(jié)構(gòu)之順序表和單鏈表
在數(shù)據(jù)結(jié)構(gòu)中,線性表是入門級數(shù)據(jù)結(jié)構(gòu),線性表又分為順序表和鏈表,這篇文章主要給大家介紹了關(guān)于C語言數(shù)據(jù)結(jié)構(gòu)之順序表和單鏈表的相關(guān)資料,需要的朋友可以參考下2021-06-06