愿文:http://blog.csdn.net/rabbit729/archive/2009/02/23/3928437.aspx
網(wǎng)絡(luò)游戲服務(wù)器開發(fā)技術(shù)
-------如何正確高效的使用內(nèi)存和對(duì)象內(nèi)存池?
大家都知道,游戲服務(wù)器在網(wǎng)絡(luò)游戲開發(fā)中所占的比重。而評(píng)論游戲服務(wù)器的好壞標(biāo)準(zhǔn),除了實(shí)現(xiàn)游戲的邏輯功能外,最重要的也就是穩(wěn)定和高效。一個(gè)不穩(wěn)定的服務(wù)器對(duì)于一款網(wǎng)絡(luò)游戲的打擊是沉重,一個(gè)不高效的服務(wù)器對(duì)于玩家的感覺也是非常明顯的。
在這一章節(jié)中,我將要向大家介紹游戲服務(wù)器高效開發(fā)的一個(gè)方面,如何正確高效的使用內(nèi)存?而關(guān)于其他高效開發(fā)的技術(shù)或者架構(gòu)設(shè)計(jì),我也將陸續(xù)向大家介紹。其中有欠妥之處,也希望讀者告訴我,我們大家一起成長(zhǎng)。
先簡(jiǎn)單說下內(nèi)存和內(nèi)存使用的通俗概念,內(nèi)存也就是一塊虛擬地址空間。而在C++程序開發(fā)過程中,我們可以直接讀\寫這個(gè)地址空間,也就是內(nèi)存使用。
接下來我們來了解下內(nèi)存的分配方式。通常情況下,內(nèi)存的分配方式有3種方式:
1) 從靜態(tài)存儲(chǔ)區(qū)域分配。內(nèi)存在程序編譯的時(shí)候就已經(jīng)分配好,這塊內(nèi)
存在程序的整個(gè)運(yùn)行期間都存在。例如全局變量,static變量。
2) 在棧上創(chuàng)建。在執(zhí)行函數(shù)時(shí),函數(shù)內(nèi)局部變量的存儲(chǔ)單元都可以在棧
上創(chuàng)建,函數(shù)執(zhí)行結(jié)束時(shí)這些存儲(chǔ)單元自動(dòng)被釋放。棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。
3) 從堆上分配,亦稱動(dòng)態(tài)內(nèi)存分配。程序在運(yùn)行的時(shí)候用malloc或new
申請(qǐng)任意多少的內(nèi)存,程序員自己負(fù)責(zé)在何時(shí)用free或delete釋放內(nèi)存。動(dòng)態(tài)內(nèi)存的生存期由我們決定,使用非常靈活,但問題也最多。
使用點(diǎn)評(píng)A:
在游戲服務(wù)器開發(fā)過程中,如果我們要在函數(shù)中需要一塊不大于4M的內(nèi)存,建議我們使用第2)種分配方式。因?yàn)檫@種方式不僅可以使應(yīng)用程序高效(分配快,且不會(huì)造成內(nèi)存碎片),而且會(huì)防止內(nèi)存泄露。(有時(shí)我們的程序員會(huì)在不經(jīng)意間忘記掉釋放自己通過new或者malloc分配的內(nèi)存))
對(duì)于三種分配方式,也就對(duì)應(yīng)了三種不同的內(nèi)存生命周期。所謂生命周期也就是從產(chǎn)生到釋放的一個(gè)過程時(shí)間段。
1) 靜態(tài)分配的內(nèi)存空間的生命周期期是:整個(gè)軟件運(yùn)行期,就是說從軟
件運(yùn)行開始到軟件終止退出。只有軟件終止運(yùn)行后,這塊內(nèi)存才會(huì)被系統(tǒng)回收 。
2) 棧中分配的內(nèi)存空間的生命周期是和相應(yīng)的函數(shù)或者內(nèi)存對(duì)象的作
用域有關(guān)。
例如函數(shù)如下:
void Func(void)
{
{ int i = 10; i++;}
int j = 0;
for(;j<100;j++){ printf("%d\n",j);}
}
對(duì)于上面函數(shù)中兩個(gè)局部變量,雖然都是從棧中進(jìn)行分配,但生命周期
是不一樣的,變量i出了花括號(hào){}后被系統(tǒng)回收,所以在函數(shù)其他地方將是無意義的。而變量j在函數(shù)結(jié)束時(shí)被系統(tǒng)回收。但他們都有一個(gè)共同點(diǎn):那就是使用者不需要去關(guān)心如何進(jìn)行分配和釋放,所有這一切都是被C++所有保證的。
3) 在堆上分配的內(nèi)存,生命周期是從調(diào)用new或者malloc開始,到調(diào)
用delete或者free結(jié)束。如果不調(diào)用用delete或者free。則這塊空間必須到軟件終止運(yùn)行后才能被系統(tǒng)回收。
使用點(diǎn)評(píng)B:
在我們寫程序過程中如果要在堆中分配內(nèi)存,必須養(yǎng)成一個(gè)良好的習(xí)慣。那就是我們首先必須建立成對(duì)的new\delete和malloc\free。對(duì)于其他使用此內(nèi)存對(duì)象進(jìn)行邏輯處理的代碼就直接放在其中間就可以了。
最后我們?cè)賮砜纯矗覀兺ǔJ褂脙?nèi)存比較容易犯的一些錯(cuò)誤。其實(shí)大家可不要小看這些錯(cuò)誤,很多時(shí)候這些錯(cuò)誤的產(chǎn)生,對(duì)于產(chǎn)品使用者的影響是巨大的。
內(nèi)存未分配成功,我們就錯(cuò)誤的使用了它。
這種情況的發(fā)生。就通常和我們沒有養(yǎng)成良好的編程習(xí)慣或者經(jīng)驗(yàn)不豐富有關(guān)。常用解決辦法是,在使用內(nèi)存之前檢查指針是否為NULL。如果指針p是函數(shù)的參數(shù),那么在函數(shù)的入口處用if(p!=NULL)進(jìn)行檢查。如果我們?cè)诤瘮?shù)中使用malloc或new來申請(qǐng)內(nèi)存,在使用前必須使用if(p!=NULL)判斷后,再進(jìn)行使用。
內(nèi)存分配雖然成功,但是尚未初始化就引用它。
犯這種錯(cuò)誤主要有兩個(gè)起因:一是沒有初始化的觀念;二是誤以為內(nèi)存的缺省初值全為零,導(dǎo)致引用初值錯(cuò)誤(例如數(shù)組)。
內(nèi)存的缺省初值究竟是什么并沒有統(tǒng)一的標(biāo)準(zhǔn),盡管有些時(shí)候?yàn)榱阒担覀儗幙尚牌錈o不可信其有。所以無論用何種方式創(chuàng)建數(shù)組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。
內(nèi)存分配成功并且已經(jīng)初始化,但操作越過了內(nèi)存的邊界。
這種情況的產(chǎn)生經(jīng)常會(huì)發(fā)生在數(shù)組下標(biāo)的錯(cuò)誤使用和對(duì)象指針的錯(cuò)誤內(nèi)存拷貝。
例如:
void Func(void)
{
char chArray[100];
for(int j=1;j<=100;j++){ chArray[j] = 100; }
}
void Func(void)
{
char *p = (char*)malloc(10);
char *pszA = "DFASDFASDFASDFASDFASDFFD";
memcpy(p,pszA,strlen(pszA));
free(p);
}
忘記了內(nèi)存釋放,造成內(nèi)存泄露。
對(duì)于這種錯(cuò)誤的產(chǎn)生原因可就多種多樣了,而最常見犯錯(cuò)誤的原因,還是大家沒有一個(gè)良好的編程習(xí)慣或者對(duì)于指針使用理解不深刻造成。
下面簡(jiǎn)單列舉大家可能犯錯(cuò)誤的幾種常見情況:
錯(cuò)誤1:
void AllocateMemory(char *pStr, int num)
{
pStr = new char[num];
}
錯(cuò)誤2:
void Func(void)
{
CObj *pOBJ = new CObj[100];
if(pStr!=NULL)
{
.........
}
delete pOBJ;
}
使用點(diǎn)評(píng)C:
錯(cuò)誤1沒有很好的理解函數(shù)對(duì)于參數(shù)的處理是為每一個(gè)參數(shù)設(shè)置一個(gè)副本,在函數(shù)中語句操縱的其實(shí)是參數(shù)的副本,而本身并沒有被改變。
錯(cuò)誤2沒有將[]配對(duì)使用,造成99個(gè)CObj對(duì)象的析構(gòu)函數(shù)沒有被調(diào)用,對(duì)象本身內(nèi)存泄露,且程序會(huì)報(bào)告異常。
釋放了內(nèi)存卻繼續(xù)使用它。
有幾種情況:
程序中的對(duì)象調(diào)用關(guān)系過于復(fù)雜,實(shí)在難以搞清楚某個(gè)對(duì)象究竟是否已經(jīng)釋放了內(nèi)存,此時(shí)應(yīng)該重新設(shè)計(jì)程序結(jié)構(gòu),理清楚對(duì)象直接的關(guān)系,解決程序混亂的問題。
函數(shù)的return語句寫錯(cuò)了,注意不要返回指向“棧內(nèi)存”的“指針”或者“引用”,因?yàn)樵搩?nèi)存在函數(shù)體結(jié)束時(shí)被自動(dòng)銷毀。
使用free或delete釋放了內(nèi)存后,沒有將指針設(shè)置為NULL。導(dǎo)致產(chǎn)生“野指針”。
錯(cuò)誤的使用棧內(nèi)存,造成棧溢出。
如果我們?cè)诤瘮?shù)中需要一定大小的內(nèi)存,從棧中分配要比new或者malloc堆內(nèi)存分
配方式要快很多,所以我們很多朋友就肆無忌憚的使用棧內(nèi)存。從而造成棧溢出。
“An unhandled exception of type 'System.StackOverflowException' occurred in Temp.exe“
這種情況的產(chǎn)生,多數(shù)是大家沒有很好的了解內(nèi)存對(duì)象生命周期而造成的。
例如:
void Func(void)
{
for(int i=0;i<10;i++){ char szBuf[1024*1024]; .....}
}
使用點(diǎn)評(píng)D:
1.使用指針之前,首先必須檢查指針指向內(nèi)存的有效性。防止使用NULL指針,造成程序崩潰。
2.分配內(nèi)存成功后,最好要對(duì)于內(nèi)存賦初值。防止將未被初始化的內(nèi)存作為右值使用,造成程序的錯(cuò)誤。
3.避免數(shù)組下標(biāo)或者指針的地址越界,特別要當(dāng)心發(fā)生“多1”、“少1”和內(nèi)存拷貝的操作。
4.動(dòng)態(tài)內(nèi)存的申請(qǐng)與釋放必須配對(duì),防止內(nèi)存泄漏。同時(shí)不能夠?qū)ew和free或者malloc和delete進(jìn)行錯(cuò)誤配對(duì)。因?yàn)檫@樣產(chǎn)生的問題將是難以預(yù)料的。
5.用free或delete釋放了內(nèi)存之后,立即將指針設(shè)置為NULL,杜絕產(chǎn)生“野指針”。
6.合理的使用棧內(nèi)存,并且需要關(guān)心局部?jī)?nèi)存對(duì)象的生命周期.從而無錯(cuò)高效的使用
棧內(nèi)存.
上面用了如此多的篇幅來向大家講述內(nèi)存的分配、釋放和使用注意點(diǎn)。只是想加深大家對(duì)于內(nèi)存使用的理解。下面我們就來繼續(xù)討論我們?cè)谟螒蚍?wù)器中如何正確高效使用內(nèi)存和建立內(nèi)存管理池。
首先,我先簡(jiǎn)單的敘述下一般游戲服務(wù)器在資源使用方面,都需要做些什么?以下簡(jiǎn)單歸納為如下幾種資源使用情況:
a) 啟動(dòng)Listen端口偵聽的Client連接請(qǐng)求。在Client連接成功后,分配一定的
對(duì)象資源和此連接對(duì)應(yīng)。
b) 接受Client的網(wǎng)絡(luò)連接斷開請(qǐng)求,釋放與此連接所對(duì)應(yīng)的所有資源。
c) 接受Client網(wǎng)絡(luò)數(shù)據(jù)包,跟據(jù)協(xié)議碼處理此數(shù)據(jù)包。處理并且釋放協(xié)議包資源。
d) 接受DB訪問,提出SQL請(qǐng)求、獲取記錄集、處理記錄集,然后釋放記錄集資源等。
e) 地圖Monster對(duì)象的產(chǎn)生和銷毀.NPC對(duì)象產(chǎn)生和銷毀等等。
………………
細(xì)心的我們,觀察以上幾點(diǎn)。我們不難發(fā)現(xiàn),在游戲服務(wù)器開發(fā)過程中,將涉及到大量的內(nèi)存資源分配和釋放。
而對(duì)于MMO或者其他休閑網(wǎng)絡(luò)游戲產(chǎn)品所涉及的各種服務(wù)器:longinserver、gamegate、dbserver、gameserver等,由于各自要處理的邏輯和擔(dān)任角色不一樣,其設(shè)計(jì)復(fù)雜度和代碼量也不盡相同,這其中以gameserver為其中之最。代碼量通常會(huì)在幾萬行到幾十萬行不等,而其設(shè)計(jì)復(fù)雜度更是根據(jù)不同游戲而定。
以上說的只是他們的開發(fā)差異,而所有服務(wù)器程序必須具備的共同點(diǎn)是:穩(wěn)定和高性能。同時(shí)我們不僅要求單個(gè)服務(wù)器的穩(wěn)定和高性能,而是要求全局服務(wù)器組的穩(wěn)定和高性能。因此,這就要求我們服務(wù)器程序員必須具體比較強(qiáng)的代碼控制能力和豐富的開發(fā)經(jīng)驗(yàn)。而這其中一個(gè)比較重要環(huán)節(jié)就是內(nèi)存的合理使用。
如果我們使用傳統(tǒng)的內(nèi)存分配方式來進(jìn)行程序設(shè)計(jì),情況將會(huì)如下所示:
使用的時(shí)候向系統(tǒng)申請(qǐng),使用后就歸還給系統(tǒng)。也正是我們所說的new\delete和malloc\free這種方式。
服務(wù)器應(yīng)用程序開啟一段時(shí)間之后,我們的系統(tǒng)使用中內(nèi)存和空閑內(nèi)存將會(huì)呈現(xiàn)如下方式分布:
這其中將會(huì)出現(xiàn)許多內(nèi)存碎片,由于內(nèi)存碎片的大量存在,我們服務(wù)器性能也會(huì)隨之降低。并且我們還必須承擔(dān)由于內(nèi)存使用不當(dāng)造成系統(tǒng)不穩(wěn)定或者內(nèi)存大量泄露的風(fēng)險(xiǎn)。
既然傳統(tǒng)的內(nèi)存編程方式在服務(wù)器程序開發(fā)和應(yīng)用過程中有這樣的一些潛在弊端。那么我們使用方式來改進(jìn)我們的服務(wù)器應(yīng)用程序性能和增強(qiáng)系統(tǒng)穩(wěn)定性呢?
大家不妨嘗試使用,我接下來大家要的內(nèi)容: 對(duì)象內(nèi)存池技術(shù).
大家一定要說了,內(nèi)存池技術(shù)前面為什么要加上“對(duì)象”呢。其實(shí)就是想和我們平常所說的內(nèi)存池技術(shù)概念進(jìn)行區(qū)別,在這里向大家講述的是狹義的內(nèi)存池技術(shù),也就是應(yīng)用層面的內(nèi)存池技術(shù)。
提示:
在VC6.0以上開發(fā)環(huán)境中,我們是基于OOP進(jìn)行游戲服務(wù)器開發(fā)。在OOP(面向?qū)ο缶幊蹋┚幊踢^程中我們習(xí)慣的將我們應(yīng)用程序中遇到的所有一切進(jìn)行抽象,成為一個(gè)概念對(duì)象。
例如:gameserver 中的玩家,我們抽象為class CPlayer,怪物,抽象為class CMonster,網(wǎng)絡(luò)協(xié)議包消息,抽象為class CNetEvent等等。
所以可以簡(jiǎn)單的說,我們的游戲服務(wù)器編程也就是對(duì)象編程。而具體這些基礎(chǔ)知識(shí)和抽象技巧,這里也不多累贅了。
在這里,還是先介紹下我們這邊對(duì)象內(nèi)存池技術(shù)的基本概念和設(shè)計(jì)過程:
所謂的對(duì)象內(nèi)存池技術(shù)設(shè)計(jì)過程如下:
首先為某種對(duì)象預(yù)先生成若干個(gè)空閑對(duì)象,并且使用對(duì)象管理類進(jìn)行管理。應(yīng)用程序在需要使用此對(duì)象時(shí),即向管理對(duì)象申請(qǐng)空閑對(duì)象.管理對(duì)象即檢視對(duì)象內(nèi)存池,如果發(fā)現(xiàn)存在未使用空閑對(duì)象,即分配給申請(qǐng)者。如果發(fā)現(xiàn)已無空閑對(duì)象,可自行擴(kuò)充對(duì)象內(nèi)存池,并且滿足申請(qǐng)對(duì)象的需求,也可以直接返回NULL,表明對(duì)象申請(qǐng)失敗。在程序獲取對(duì)象并且使用后,想釋放此對(duì)象資源時(shí)。繼續(xù)想管理對(duì)象提出申請(qǐng)釋放對(duì)象,管理對(duì)象接受到釋放對(duì)象后將其再次放入對(duì)象池,成為可使用對(duì)象。
看了上面的介紹后,接下來以偽代碼的方式來更加清晰的展現(xiàn)對(duì)象獲取和釋放的過程。
申請(qǐng)對(duì)象
OBJ* ApplyObj(void)
{
if 存在空閑對(duì)象
{
OBJ *pIdleObj = NULL;
pIdleObj = GetIdleObj(); //獲取空閑對(duì)象
return pIdleObj;
}
//不存在空閑對(duì)象,處理方式如下
方式1:
ExtendObjectPool(); //擴(kuò)充對(duì)象池
OBJ *pIdleObj = NULL;
pIdleObj = GetIdleObj(); //獲取空閑對(duì)象
return pIdleObj;
方法2:
return NULL;
}
釋放對(duì)象
void ReleaseObj(OBJ* pObj)
{
if(pObj!=NULL)
{
AddToObjectPool(pObj); //對(duì)象再次加入到對(duì)象池
}
}
有了上面的這些說明,我想大家對(duì)于對(duì)象內(nèi)存池技術(shù)應(yīng)該都有了一個(gè)大概了解吧!(其實(shí)沒有什么高深的技術(shù),只是一些簡(jiǎn)單的應(yīng)用,大家用一個(gè)平常心來看待就可以了!)接下介紹具體來實(shí)現(xiàn)這個(gè)對(duì)象內(nèi)存池,我們需要做些什么?
在實(shí)現(xiàn)對(duì)象內(nèi)存池之前,先提出幾個(gè)我們需要達(dá)到的目標(biāo):
對(duì)象內(nèi)存池管理對(duì)象具有廣泛的通用性,也就是說能夠滿足應(yīng)用程序生成各個(gè)不同的對(duì)象池,例如:Pl(wèi)ayer對(duì)象池、Monster對(duì)象池、NPC對(duì)象池。
產(chǎn)生對(duì)象的速度一定要快于直接使用new\delete或者malloc\free方式很多倍。
對(duì)象池容量具有可擴(kuò)展性和糾錯(cuò)能力。也就是說在無空閑對(duì)象時(shí),管理對(duì)象類能夠自動(dòng)生成一批新的空閑對(duì)象供上層使用,同時(shí)能夠正確指出目前所使用對(duì)象是否為合法對(duì)象池對(duì)象。
對(duì)象的申請(qǐng)和釋放,必須具備多線程安全性。也就是說在服務(wù)器程序通過各個(gè)不同線程同時(shí)訪問對(duì)象池管理時(shí),能夠保證合法獲取和釋放池對(duì)象。
基于上面這些問題條件,這里先提供幾種簡(jiǎn)單易行的解決方案來供大家參考。其他解決方案還有很多,我也就不一一列舉了!靠大家獨(dú)立思考和發(fā)揮了。:)
解決方案1:(單鏈表實(shí)現(xiàn))
第一步,分配模板對(duì)象數(shù)組,并且用指針保存。
第二步,建立一單鏈表管理類,將已經(jīng)分配成功的數(shù)組對(duì)象,分配到蛋鏈表中,同時(shí)設(shè)置表頭和表尾指針。
第三步,從對(duì)象池中申請(qǐng)對(duì)象,首先檢測(cè)鏈表中是否存在可使用空閑對(duì)象。如果沒有可返回NULL,也可以先鎖定擴(kuò)充鏈表(保存擴(kuò)展對(duì)象數(shù)組指針)。然后返回可使用對(duì)象給用戶。
第四步,釋放對(duì)象池對(duì)象時(shí),首先將表尾指針指向被釋放對(duì)象,接下來被回收對(duì)象為此鏈表表尾。完成釋放過程。
最后,內(nèi)存釋放,delete數(shù)組指針。
圖例演示如下:
解決方案2:(雙鏈表實(shí)現(xiàn))
為了能夠使我們建立的對(duì)象池能夠在應(yīng)用程序中通用,我們考慮使用模板template<class OBJ>來生成我們的class CObjectPool.
為了能夠快速獲取對(duì)象,我們采用雙向鏈表的方式來建立我們的對(duì)象池。對(duì)象獲取從當(dāng)前鏈表頭開始進(jìn)行,對(duì)象釋放直接加到鏈表尾。操作過程中需要使用一附加指針對(duì)象表明當(dāng)前可使用對(duì)象位置。若此指針為NULL,表明已無可使用空閑對(duì)象。
為了生成一個(gè)一定容量的對(duì)象池,我們可以通過模板的方式也可以通過初始化Init方式來生成初始對(duì)象池。在申請(qǐng)過程中無空閑對(duì)象,需要向系統(tǒng)重新一定數(shù)目對(duì)象,并且按照順序加到鏈表尾。實(shí)現(xiàn)對(duì)象池的可擴(kuò)充性。
為了保證多線程安全,我們?cè)趯?duì)象申請(qǐng)和釋放過程中加入Lock進(jìn)行鎖定,保證每次只有一個(gè)線程操作對(duì)象池。
以上就為此對(duì)象池實(shí)現(xiàn)的解決方案,在了解到實(shí)現(xiàn)過程的前提下,具體實(shí)現(xiàn)代碼這里也就不累贅了。大家可以發(fā)揮實(shí)現(xiàn),如果實(shí)現(xiàn)過程中出現(xiàn)問題可以直接和我聯(lián)系。
重申:
游戲服務(wù)器編程不是一個(gè)多高深的程序設(shè)計(jì)課題,設(shè)計(jì)和開發(fā)高質(zhì)量的網(wǎng)絡(luò)游戲服務(wù)器程序也不是那么的可怕和難以攀登。要的是我們?cè)陂_發(fā)過程中能夠多吸收前人所積累的經(jīng)驗(yàn),杜絕將前人錯(cuò)誤進(jìn)行重演。同時(shí)要的是我們?cè)鷮?shí)的C++基礎(chǔ)功底、認(rèn)真嚴(yán)謹(jǐn)?shù)拈_發(fā)態(tài)度和對(duì)于游戲開發(fā)事業(yè)的120%熱情。我們決不要做被程序開發(fā)左右的人,要做左右程序開發(fā)的人(汗中….)。另外提醒游戲服務(wù)器開發(fā),力求穩(wěn)定實(shí)用,個(gè)人不建議在代碼中使用過多的技巧,同時(shí)STL部分建議適量使用。
本文來自CSDN博客,轉(zhuǎn)載請(qǐng)標(biāo)明出處:http://blog.csdn.net/rabbit729/archive/2009/02/23/3928437.aspx
posted on 2009-06-17 00:38
水 閱讀(1577)
評(píng)論(0) 編輯 收藏 引用 所屬分類:
內(nèi)存管理