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