我一直堅信,如果不是處理大規(guī)模客戶端連接,是不需要使用epoll和IOCP的。我傾向于簡單的東西,所以我一直用著select。
一直以來,我的網(wǎng)絡(luò)程序結(jié)構(gòu)就是在每一幀的開始select,有什么消息就處理一下,然后跑程序的主邏輯。我覺得這個結(jié)構(gòu)挺好,單線程,簡單、明了、優(yōu)雅。
不過最近有頭兒告訴我,這個事情雖然可以,但是感覺上不太對頭,網(wǎng)絡(luò)組件的工作應(yīng)該是獨立的,不可以占用主邏輯的時間。
好吧,我改成多線程就是了。一個主線程,負(fù)責(zé)處理客戶端消息和運算主邏輯;一個網(wǎng)絡(luò)線程,負(fù)責(zé)從網(wǎng)絡(luò)上讀取數(shù)據(jù)和將數(shù)據(jù)發(fā)送到網(wǎng)絡(luò)上,基本上就是select,recv,send三調(diào)用。
這里出現(xiàn)了第一個問題:主線程和網(wǎng)絡(luò)線程之間如何進行數(shù)據(jù)交換?主線程中待發(fā)送的數(shù)據(jù)需要交給網(wǎng)絡(luò)線程做實際的發(fā)送,網(wǎng)絡(luò)線程接收到的數(shù)據(jù)需要交給主線程處理。
基于盡可能短的lock-time這一原則,我給主線程和網(wǎng)絡(luò)線程各分配了一個完全一樣的容器,這個容器由各線程獨自享有,容納發(fā)送和接收的數(shù)據(jù)。
然后在特定的時候,lock主其中一個容器,進行數(shù)據(jù)拷貝即可——將欲發(fā)送的數(shù)據(jù)從主線程容器拷貝到網(wǎng)絡(luò)線程的容器,將收到的數(shù)據(jù)從網(wǎng)絡(luò)線程的容器拷貝到主線程的容器。
這一lock只有拷貝工作,時間上應(yīng)該是十分短暫的。
第二個問題:由誰負(fù)責(zé)這個拷貝,主線程還是網(wǎng)絡(luò)線程?負(fù)責(zé)拷貝的線程,必然去lock另一個線程的容器。
我選擇了主線程負(fù)責(zé)拷貝操作。在每幀的開始,鎖住網(wǎng)絡(luò)線程的容器,將它收到的數(shù)據(jù)拷出來,將要發(fā)送的數(shù)據(jù)拷進去,解鎖,然后處理收到的消息。網(wǎng)絡(luò)線程則需要在操作自己的容器的時候加鎖。
好處是,主線程的 send_packet 操作不需要加鎖,并且收到的數(shù)據(jù)是拷出來就消耗掉。
順便也想想網(wǎng)絡(luò)線程負(fù)責(zé)拷貝的情形,在select之前,鎖住主線程的容器,將欲發(fā)送的數(shù)據(jù)拷進,解鎖;然后是select,recv,send;然后再次鎖住主線程的容器,將收到的數(shù)據(jù)拷出。相應(yīng)的,主線程需要在操作自己容器的時候加鎖。
看起來我并不想主線程在一幀內(nèi)有太多次的加鎖解鎖操作,因此就選擇了第一個方案。
至此,程序跑起來了。不過出現(xiàn)了一個單線程所沒有的新問題——CPU占用率太高了。
原因應(yīng)該是,select能掛起程序,所以單線程的時候,程序多多少少總會有掛起的機會;但是多線程以后,主線程就跟while ( true )差不多,浪費了太多的資源。
因此,讓主線程在每幀也睡一會就好了。游戲的主邏輯是限幀的,一般每秒25幀,稱邏輯幀。但是處理網(wǎng)絡(luò)消息不是限幀的,而是希望能盡可能快的處理他們,因此處理網(wǎng)絡(luò)消息是在實際幀中進行的。
通常游戲主邏輯的一次tick并不能完全消耗掉一個邏輯幀的時間,因此讓主線程在邏輯幀剩下的時間里睡上一覺就好。
第三個問題是:如何讓主線程在剩下的邏輯幀時間里掛起,并在有網(wǎng)絡(luò)消息的時候立即激活?
信號/EVENT——主線程在進行容器的數(shù)據(jù)拷貝之前,如果自己沒有欲發(fā)送的數(shù)據(jù),則等待信號,等待的時間是上一個邏輯幀所剩余的時間。相應(yīng)的網(wǎng)絡(luò)線程中,如果收到新的數(shù)據(jù),則激活這個信號,那么主線程會被立即喚醒。
等待超時或者被喚醒后,就會執(zhí)行數(shù)據(jù)拷貝和消息處理。這樣,既實現(xiàn)了sleep,又兼顧了即時反應(yīng)能力。
編譯運行,程序看起來挺穩(wěn)定,CPU占用率為0.。。。。。。新項目,邏輯上幾乎啥都沒有呢。
posted on 2009-01-03 16:54
LOGOS 閱讀(7946)
評論(8) 編輯 收藏 引用