Dirty Flag 模式及其應用


  之前在開發中就發現“dirty”是一種不錯的解決方案:可以用來延緩計算或者避免不必要的計算。后來在想,這應該也算一種設計模式吧,於是搜索“Dirty設計模式”,沒有什么結果,然后換成英文“Dirty design pattern”,搜到了《game programming patterns》這本電子書。書中介紹了Dirty Flag 模式在游戲客戶端的應用場景,如果英文不好,這里也有中文翻譯。本文結合幾個具體的例子,介紹什么是Dirty Flag 模式,並分析該模式的適用場景以及使用注意事項。

什么是Dirty Flag:

  簡單來說,就是用一個標志位(flag)來表示一組數據的狀態,這些數據要么是用來計算,或者用來需要同步。在滿足條件的時候設置標志位,然后需要的時候檢查(check)標志位。如果設置了標志位,那么表示這組數據處於dirty狀態,這個時候需要重新計算或者同步。如果flag沒有被設置,那么可以不計算(或者利用緩存的計算結果)。另外,在兩次check之間,即使有多次標志位的設置,也只需要計算一次。

  因此,Dirty Flag模式的本質作用在於:延緩計算或數據同步,甚至減少無謂的計算或者同步。計算比較容易理解,對於同步,后面也會給出例子。在后面的描述中,除非特殊說明,計算也包含了同步。

Dirty Flag使用實例:

  首先,《game programming pattern》中的例子非常形象生動,圖文並茂,建議直接閱讀原文,本文不再復述。接下來介紹幾個其他的例子。

First

    游戲開發中,有大量的物體(Entity)需要每幀tick(AI、位移),每次tick的時候檢查一些條件然后做出反應。對於一些entity,可能tick檢查之后發現什么都不用做,但每次tick檢查也比較耗時,而且出現這種情況的概率還很高。
    利用dirty可以改造一些
1 def set_need_tick(self, is_need):
2     self.need_tick = is_need
3 
4 def tick(self):
5     if self.need_tick:
6         self.do_tick() # do_tick 需要做大量的檢查,較為耗時
  上面的代碼每次調用tick的時候用一次條件判斷,還可能嵌套一次函數調用,修改后代碼如下:
1 def dummy_tick(self):
2     pass
3 def set_need_tick(self, is_need):
4     if is_need:
5         self.tick = self.do_tick
6     else:
7         self.tick = self.dummy_tick
    不過上述的代碼也是空間換時間,因為每個實例都增加了一個tick屬性(原來是類屬性)

Second

  之前在看bottle的代碼時,看到了下面這個property,其作用是在首次調用的時候計算屬性的值,之后就不用重新計算了。
 1 class cached_property(object):
 2     """ A property that is only computed once per instance and then replaces
 3         itself with an ordinary attribute. Deleting the attribute resets the
 4         property. """
 5  
 6     def __init__(self, func):
 7         update_wrapper(self, func)
 8         self.func = func
 9  
10     def __get__(self, obj, cls):
11         if obj is None: return self
12         value = obj.__dict__[self.func.__name__] = self.func(obj)
13         return value
  如果一時不能理解上面的代碼,可以參見這篇文章《 python屬性查找》。bottle中這個例子,前提是這個屬性一旦計算了就不會再重新計算,如果應用場景在某些情況下需要重新計算呢?那么可以增加這么一個函數:
1     def set_property_dirty(self, property_name):
2         self.__dict__.pop(property_name, None)

  在需要的時候調用這個設置函數就行了,在這個例子中,並沒有對某個屬性的設置和檢查,但配合之前的cached_property,作用是很明顯的:緩存計算結果,需要的時候重新計算。下面是完整測試代碼

  
 1 import functools, time
 2 class cached_property(object):
 3     """ A property that is only computed once per instance and then replaces
 4         itself with an ordinary attribute. Deleting the attribute resets the
 5         property. """
 6 
 7     def __init__(self, func):
 8         functools.update_wrapper(self, func)
 9         self.func = func
10 
11     def __get__(self, obj, cls):
12         if obj is None: return self
13         value = obj.__dict__[self.func.__name__] = self.func(obj)
14         return value
15 
16 class TestClz(object):
17     @cached_property
18     def complex_calc(self):
19         print 'very complex_calc'
20         return sum(range(100))
21 
22     def __set_property_dirty(self, property_name = 'complex_calc'):
23         self.__dict__.pop(property_name, None)
24 
25     def some_action_effect_property(self):
26         self.__set_property_dirty()
27 
28     
29 
30 if __name__=='__main__':
31     t = TestClz()
32     print '>>> first call'
33     print t.complex_calc
34     print '>>> second call'
35     print t.complex_calc
36     print '>>>third call'
37     t.some_action_effect_property()
38     print t.complex_calc
cache property and dirty

 

Third

  游戲數據存檔,游戲中的大量對象都需要持久化(存檔),存檔有各種不同的策略。第一種,每次屬性變化的時候存檔,這樣數據不容易丟失,但是往往會存在冗余,數據庫壓力也較大,比如屬性A的變化影響到屬性B(經驗的增加導致等級的變化),那么屬性A變化的時候進行一次存檔,屬性B變換的時候又要存檔。另一種策略是,定期存檔,即以固定的時間間隔進行一次存檔,當然可以進一步,在需要持久話的數據變化時設置dirty flag,在定期存檔的時機只有設置了dirty flag才真正存儲。
 

Fourth

  這個例子是Dirty Flag的升級版本,暫且稱之為tag Flag吧。比如頁面上有一些圖表,圖表是通過大量數據的數據計算然后繪制出來的,圖表內容可以通過用戶主動點擊刷新或者定時刷新。簡單的策略是每次刷新的時候服務器返回所有數據,瀏覽器重新顯示。但事實上服務端數據變化可能不那么頻繁,既浪費了大量的帶寬,又讓瀏覽器無謂的重復繪制。
  其中一種解決的辦法,就是為數據增加一個簽名--tag(自增整型),對於服務器端,每次數據變化的時候tag += 1。瀏覽器首次請求的時候獲取數據以及當前數據對應的tag,之后請求的時候攜帶tag與服務端的tag做比較,若tag一致,則無需更新數據,否則返回新的數據和新的tag。這樣,所有的客戶端都能利用這個tag來決定自己是否要刷新數據。
 

Fifth

  Dirty在web前端還有許多其他應用,比如 angularJSKnockoutJS,由於本人並不熟悉web前端,感興趣的讀者可以參考鏈接

適用場景:

    正如《game programming patterns》中的歸類, Dirty Flag pattern屬於optimization pattern,只有需要優化的時候才考慮使用該模式。有人說,”過早的優化是萬惡之源“,我覺得這對於Dirty Flag還是比較合適的,Dirty Flag的使用不是那么自然,跟業務邏輯本身也是無關的,只有在Profile確定瓶頸之后再來考慮是否可以用Dirty Flag優化。
    某些計算(或者同步)較為昂貴且頻繁,但事實上很多情況無需計算(或者同步),通過Dirty Flag來標志真正需要計算(或者同步)的情況,降低開銷。
 

使用條件:

  當滿足下面所有條件,或者說權衡下面的所有條件之后還可以接受,那么才建議使用Dirty Flag模式。
第一:單次計算的開銷足夠大
    這個是首要條件,如果每次計算的開銷非常小,那么就沒有必要用Dirty Flag來優化了,因為增加標志位既增加了編碼復雜度,又帶來了一定的開銷(標志位的設置與清除)。單次計算的開銷可以通過profile來確定。
 
第二:事實上需要計算的概率足夠低
  我們還是以游戲為例,假設游戲的是60幀,即每秒tick60次。如果一個計算每次(每幀)都有很大的概率“必須 重新計算,那dirty flag反而增加了額外的開銷。
 
第三:延遲計算沒有副作用
  如《game programming pattern》中的例子所示,Dirty Flag在這個例子導致延遲計算,延遲計算會將分散在不同時間進行的計算集中到檢查的時刻,這樣可能帶來一些副作用,比如造成游戲卡頓。另外,對於游戲存檔的例子,如果在兩次定期存儲之間服務器宕機,可能會數據丟失。
 
第四:內存換速度的代價
    使用dirty模式,很多時候需要緩存結果,這需要額外的內存,對於某些場景,還有緩存命中率的問題,在內存稀缺的移動設備上尤其需要權衡。
 

注意事項:

第一:標志位的設置必須覆蓋到所有可能影響的地方
  如果某些操作遺漏了對標志位的位置,那么往往會導致嚴重的錯誤。這個也跟標志位的粒度有關。大多數情況都是因為對某個屬性的修改,導致需要重新計算,在python語言中要監控到屬性的修改還是很容易的,可以蟲子__setattr__函數,或者使用property descriptor
 
第二:計算之后reset標志位
 
 
references:


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM