2009年10月27日 星期二

多重Singleton時的問題,與嘗試解決

一樣是寫遊戲時遇到的狀況。因為越來越複雜了,不寫下來搞不好過一段時間我就忘了。(關於Singleton是什麼我就不在這邊介紹了,請參閱「Design Pattern」,有中文版)

首先是一開始。我的遊戲需要以PHP提供的標準函式庫連接SQL資料庫,其中需要保管該次連接唯一的connection。最早是在最上層開一個connection,然後逐層傳下去。想也知道這樣缺乏效率(主要是每個function都需要有那麼一個傳入欄位),所以後來改的作法是採用Singleton。要的function自己去拿一份reference出來。

我的Singleton的實作方式是將reference以static member的形式存在class中,然後每次需要時就去拿該object的reference出來。PHP的object是在沒有人reference到他時才釋放。因此在這裡,該object的release是在全部程式碼執行完要釋放static member的Shutdown階段。



##ReadMore##
好了,SQL connection的問題解決,再來是SQL的table(跟row)。我把SQL各table取名為XxxxDB(xxxx是table名),例如:CharDB ,BattleDB。每個object代表一個row。construct的時候讀出資料存入row,destruct的時候檢查有哪些值被更新了,寫入資料庫後才結束。

這樣的作法運作正常,但有個問題:在多層的function call裡面,容易造成資料不同步的情形。比如說現在的狀況是,function a呼叫b然後b呼叫c。在a跟b跟c裡面都各自造出了CharDB的同一個row的object。c裡面那個object改動了數值,並在離開c的scope後寫入了資料庫。但a跟b裡面持有的還是舊的資料,並沒有跟資料庫同步到。

在這邊一開始要寫Singleton時遭遇到困難(後述),因此採用的是別的方法。那時最後的作法是當c的值更新時,他會去通知a跟b之後要記得取得新值。

這個做法彆扭了些,而且某些狀況下需要手動處理。(想一下上面的a,b,c的狀況,但是這次是a在呼叫b之前造出該object並改動了值。在這裡a還沒有到scope結束所以不會寫入資料庫,但不寫入資料庫b就得用錯誤的資料。因此,a得手動進行寫入)因此我又開始想Singleton的作法了。



在這邊講一下XxxxDB也用Singleton時的問題。如上所述,Singleton的object,釋放時機是Shutdown階段。可是我們現在用到了兩類用途不同的class。其中一個會用到另一個。XxxxDB在釋放時有可能會更新資料到資料庫,因此這時SQL connection不能被釋放。但是這兩個都是Singleton...

所以看來我們得控制Shutdown階段,release的順序了。有人可能想到,在XxxxDB的object裡面存放一個SQL connection的reference如何?嗯,我試過了,不過顯然Shutdown階段release時不會去管還有沒有reference。所以SQL connection還是有可能在XxxxDB之前釋放,失敗。另外,PHP沒有辦法讓你調整Shutdown階段釋放資源的順序。(我懷疑有哪個程式語言有)



後來我找到的作法是,標準函式庫的register_shutdown_function()。這個函式會讓你註冊一到數個function,並在Shutdown開始前依序呼叫它們。所以我利用這個函式,在Shutdown開始前把各XxxxDB來自static member的reference清掉。這樣,object的reference就斷光並開始自動release。XxxxDB在Shutdown開始前release完畢,SQL connection則在Shutdown時release。順序正確。



不過有那麼簡單我就不會想發這篇了。新的問題是出在一個名為BattleDB的class。因為Web Game的某些特性,單場戰鬥的資料不能只放在記憶體還得寫入資料庫,而這就是對應的class。

BattleDB的重點在於,為了操作方便,他存有兩個CharDB的object的reference。然後還有一個名為BattleAct的class。這是為了處理行動回數等資料方便而造出的class。每個BattleDB的object持有一個BattleAct的object。為了方便,每個BattleAct也同樣持有對應的BattleDB的object(的reference)。而在我將XxxxDB處理的部份改為Singleton後,BattleDB這邊動作出了問題。

我原本以為是CharDB的object的reference釋放上的問題,不過後來發現不是。重點在於BattleAct持有的BattleDB object reference。想像一下。原本BattleDB object是在Shutdown階段前,因為register_shutdown_function()的呼叫而切斷static member的reference。釋放。但是現在BattleAct還有它的reference。而BattleAct object本身的reference被BattleDB object持有,也不會自動釋放。所以變成得到Shutdown,釋放所有資源時才release。若這時SQL connection已經release,自然的問題就出現了。



問題的解法呢。目前的作法是每次BattleDB用完時就呼叫自訂函式Close(),在裡面把BattleAct釋放掉。幸好BattleDB使用到的時機挺單純的。不過當然了,這樣做彆扭又可能對未來的擴充造成影響,之後可能還是得改。一個做法是把BattleAct併回BattleDB內部,取消相互reference問題。

這次的教訓是:注意相互reference的dead lock...尤其在記憶體自動回收的程式語言中。

沒有留言: