極不和諧的 fork 多線程程序

原文地址http://blog.codingnow.com/2011/01/fork_multi_thread.html

極不和諧的 fork 多線程程序

繼續前幾天的話題。做夢幻西遊服務器優化的事情。以往的代碼,定期存盤的工作分兩個步驟,把 VM 裏的動態數據序列化,然後把序列化後的數據寫盤。這兩個步驟,序列化工作並沒有獨立在單獨線程/進程裏做,而是放在主線程的。IO 部分則在一個獨立進程中。

序列化任務是個繁瑣的過程。非常耗時(相對於 MMORPG 這個需要對用戶請求快速反應的環境)。當玩家同時在線人數升高時,一個簡便的優化方法是把整個序列化任務分步完成,分攤到多個心跳內。這裏雖然有一些數據一致性問題,但也有不同的手段解決。

但是,在線人數達到一定後,序列化過程依然會對系統性能造成較大影響。在做定期存盤時,玩家的輸入反應速度明顯變大。表現得是遊戲服務器週期性的卡。爲了緩解這一點,我希望改造系統,把序列化任務分離到獨立進程去做。

方法倒是很簡單,在定期存盤一刻,調用 fork ,然後在子進程中慢慢的做序列化工作。(可以考慮使用 nice)做完後,再把數據交到 IO 進程寫盤。不過鑑於我們前期設計的問題,具體實現中,我需要通過共享內存把序列化結果交還父進程,由父進程送去 IO 進程。

因爲 fork 會產生一個內存快照,所以甚至沒有數據一致性問題。這應該是一個網絡遊戲用到的常見模式。

可問題就出在於,經過歷史變遷,我們的服務器已經使用了多線程,這使得 fork 子進程的做法變的不那麼可靠,需要自己推敲一下。


多進程的多線程程序,聽起來多不靠譜。真是閒得淡疼的人才會做此設計。但依舊可以使用萬能的推辭:歷史造成的。

在 POSIX 標準中,fork 的行爲是這樣的:複製整個用戶空間的數據(通常使用 copy-on-write 的策略,所以可以實現的速度很快)以及所有系統對象,然後僅複製當前線程到子進程。這裏:所有父進程中別的線程,到了子進程中都是突然蒸發掉的。

其它線程的突然消失,是一切問題的根源。

我之前從未寫過多進程多線程程序,不過公司裏有 David Xu 同學(他實現維護着 FreeBSD 的線程庫)是這方面的專家,今天跟徐同學討論了一下午,終於覺得自己搞明白了其中的糾結。嗯,寫點東西整理一下思路。

可能產生的最嚴重的問題是鎖的問題。

因爲爲了性能,大部分系統的鎖是實現在用戶空間的。所以鎖對象會因爲 fork 複製到子進程中。

對於鎖來說,從 OS 看,每個鎖有一個所有者,即最後一次 lock 它的線程。

假設這麼一個環境,在 fork 之前,有一個子線程 lock 了某個鎖,獲得了對鎖的所有權。fork 以後,在子進程中,所有的額外線程都人間蒸發了。而鎖卻被正常複製了,在子進程看來,這個鎖沒有主人,所以沒有任何人可以對它解鎖。

當子進程想 lock 這個鎖時,不再有任何手段可以解開了。程序發生死鎖。

爲何,POSIX 指定標準時,會定下這麼一個顯然不靠譜的規則?允許複製一個完全死掉的鎖?答案是歷史和性能。因爲歷史上,把鎖實現在用戶態是最方便的(今天依舊如此)。背後可能只需要一條原子操作指令即可。大多數 CPU 都支持的。fork 只管用戶空間的複製,不會涉及其中的對象細節。

一般的慣例,多線程程序 fork 前,應該由發起 fork 的線程 lock 所有子進程可能用到的鎖,fork 後,把它們一一 unlock 。當然,這樣的做法就隱含了鎖的次序。如果次序和平時不同,那麼就會死鎖。

不光是顯式的使用鎖,許多 CRT 函數也會間接的使用。比如 fprintf 這些文件操作。因爲對 FILE * 的操作是依靠鎖來達到線程安全的。最常見的問題是在子線程裏調用 fprintf 寫 log 。

除此之外,就是要小心一些不依賴鎖的數據一致性問題了。比如若在父進程裏另一個線程中操作一個鏈表,fork 發生時,因爲其它線程的突然消失,這個鏈表就可能會因爲只操作了一半而是不完整的數據。不過這一般不會是問題,或者可以歸咎於對鎖的處理。(多個線程,訪問同一塊數據。比如一條鏈表。就是需要加鎖的)

最後引用討論中, David Xu 的話 “POSIX這個問題一直是討論的熱門話題。而且雙方立場很清楚,一方是使用者,另外一方是實現者,雙方互相指責”


突然想到,lua / java 這些 VM 的實現,是不是可以利用 fork 來緩解 gc 造成的停滯呢?只需要在 gc 時,fork 一份出來做掃描。找到不被引用的垃圾,

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章