Nebula3的代碼運行在兩種根本不同的方案中. 第一種方案我稱之為”Fat Thread”. 一個Fat Thread在一個線程中運行一個完整的子系統(如渲染, 音頻, AI, 物理, 資源管理), 并且基本上鎖定在一個特定的核心上.
第二種類型的線程我叫它”Job”. 一個job是一些數據和用于處理這些數據的包裝成C++對象的代碼. 工作調度程序掌管了Job對象, 并且把工作分配給低負載的核心來保持它們一直處于忙碌狀態.
顯然, 挑戰就是設計一個經過全面考慮的系統, 以保持所有的核心一直均勻地忙碌著. 這不但意味著連續的活動需要在游戲每幀的空閑時期內輪流交替, 而且要求job對象不得不事先(如每幀前)創建好, 這樣才能在各種Fat Thread空閑時填充當前幀的空白.
這是我希望進行更多試驗和調整的地方.
第二個挑戰就是讓程序員的工作盡量的簡單. 一個游戲應用程序員(邏輯程序員)在任何時候都不應該關心他運行在一個多線程的環境中, 不應該擔心會產生死鎖或改寫了其它線程的數據, 也不應該瞎搞一些臨界區, 事件和信號量. 同樣, 整個引擎的架構也不應該是”脆弱的”. 大部分傳統的多線程代碼在一定程度上都會發生紊亂, 或者忘記了臨界區而打亂數據.
當線程間需要進行數據共享和通信時, 多線程就變得很棘手. 像兩個臨界區這樣的解決方案也會導致脆弱代碼問題.
從大的角度來說, Nebula3通過一個”并行Nebula”的概念解決了這個兩個問題. 其思想就是運行了一個完整子系統的”Fat Thread”都有自己的最小Nebula運行庫, 這個最小運行庫剛好包含了這個子系統需要的部分. 因此, 如果這個運行在它自己線程中的子系統需要進行文件訪問, 它會有一個跟其它Fat Thread完全分離的文件服務器(file server). 這個解決方案的優點是, 大部分Nebula中的代碼都不需要知道它運行在一個多線程的環境中, 因為在fat thread之間沒有數據進行共享. 運行著的每個最小Nebula內核是跟其它Nebula內核完全隔離的. 缺點就是, 重復的數據會浪費一些內存, 但是我們只是占用幾KB, 而不是MB.
這些數據冗余消除了細密的鎖定, 并且解決把程序員從思考每一行代碼的多線程安全性中解放了出來.
當然, 從某種意義上說Fat Thread間的通信是肯定會發生的, 要不然這整個思想就沒有意義了. 方法就是建立一個且只有一個的標準通信系統, 并且保證這個通信系統是可靠而快速的. 這就是消息系統的由來. 要跟一個Fat Thread通信的話只有發送一個消息給它. 消息是一個簡單的C++對象, 它包含了一些帶有get/set方法的數據. 通過這個標準的通信手段, 實際上只有消息子系統才需要是線程安全的(同樣, 訪問跟消息相關的資源時, 如內存緩沖區, 必須受到約束, 因們它們代表了共享數據). (xoyojank: 我說咋那么多Message…)
這樣雖然解決了Fat Thread方案中大多數的多線程問題, 但沒有解決Job對象的任何事情. Nebula3很有可能需要約束一個Job對象能做什么和不能做什么. 最直接的行為就是限制job做內存緩沖區的計算. 那樣的話, job中就不能存在復雜的運行庫(不能文件I/O, 不能訪問渲染等等). 如果這樣還不夠的話, 必須定義一個”job運行時環境”, 就像Fat Thread中的那樣. 因為一個job不會發起它自己的線程, 而且還會被調度到一個已經存在的線程池中. 就這個方面來說, 這不存在什么問題.
到現在為止(xoyojank: 2007/01/21, 最新版本已經實現了多數子系統的多線程化), 只有IO子系統作為概念證明在Fat Thread中得到實現, 并且它運行得很今人滿意. 在做傳統的同步IO工作時, 一個Nebula3程序可以直接調用本地線程的IO子系統. 所以像列出文件夾的內容或刪除一個文件, 只會調用一個簡單的C++方法. 對于異步IO工作, 定義了一些常見的IO操作消息(如ReadStream, WriteStream, CopyFile, DeleteFile, 等等). 進行異步IO只需要幾行代碼: 創建一個消息對象, 填充數據, 并發送這個消息到一個IOInterface單件. 如果必要的話, 這可能會需要等待和輪詢異步操作.
這樣的好處就是, 整個IO子系統沒有一行多線程意義上的代碼, 因為各個在不同的Fat Thread中的IO子系統是完全隔離的(當然, 同步肯定會發生在一些IO操作上, 但那都留給操作系統了).