shared pointer 和 weak pointer

全部程式都用 shared pointer 存物件的話,可以避免用到 dangle pointer 造成程式掛掉。特別是有用 event loop 時,用 shared pointer 比較不會出錯。但是用 shared pointer 得處理其它問題:

  • 因 circular reference 造成 memory leak。
  • 刪除物件的時機不明確。

如果不會在 destructor 作複雜的事,第二點到是無所謂。像 Java 和 Python 就不保證呼叫 “destructor” (兩者用不同名稱表示) 的時機,因此最佳實踐都建議不要用,通常也沒人用。另一方面,第一點會影響到軟體能否常期穩定執行,需要處理。

circular reference 其實比想像中來得容易發生,比方說 observer pattern 裡面的 Subject 需要保留 Observer 的指標,如果 Observer 也有保留 Subject 的指標,就會造成 circular reference (雙方都用 shared_ptr 的情況)。

有沒有可能讓 Subject 僅保留 Observer 的指標,但不影響 Observer 的 reference count?並且在 Observer 被刪除後,能判斷 Observer 已無法再使用?

weak pointer 就是為此而生的。概念如此,不過各家實作的方式不同,效能和使用限制也不同。

Java

Java 的 reference 可多了,見官方文件,以及Java引用总结 — StrongReference、SoftReference、WeakReference、PhantomReference》附的測試程式碼和該文最後的摘要。

WeakReference 的介面設計如下:

  • constructor 收目標物件直接產生 WeakReference 物件。
  • 使用前先用 get() 取得目標物件 (傳回的物件會影響 reference count),再使用傳回的物件。如果物件已被刪除,get() 會回傳 null。
  • get() 是 thread-safe 。文件上沒有討論這點,不過 JVM 要 GC 前會先 stop-the-world,應該不用擔心。

C++11 weak_ptr

使用方式和 Java 差不多:

  • 用 operator= 或 constructor 收 shared_ptr 產生 weak_ptr。
  • 使用前用 lock() 取得 shared_ptr。
  • 文件這篇所言,lock() 是 atomic,所以是 thread-safe。

透過 atomic operation 而不是 lock object,效率會好很多。有稍微看一下 GCC 的實作,不過有點複雜,沒有參透全部。有機會再仔細研究吧。

Chromium weak_ptr

使用方式和前兩者不同,WeakPtr 的介面如下:

  • class Foo 需要存在 WeakPtr 內的話,需要新增一個 member WeakPtrFactory (建議放在 member field 的最後一個,這樣它的 destructor 會最先被呼叫,提早無效化發出去的 WeakPtr),或繼承 SupportsWeakPtr。關鍵是 Foo 要有 internal::WeakReferenceOwner 管理 WeakReference,WeakPtr 會用 WeakReference 檢查目標物件是否仍能使用。
  • 以使用 WeakPtrFactory 的方式為例,用 WeakPtrFactory::GetWeakPtr() 產生 WeakPtr。
  • 使用 WeakPtr 方式和使用一般指標一樣,使用前先檢查 WeakPtr 是否為 null,不是 null 再存取它的 member fields/functions。
  • 粗略來說,WeakPtr 是 non-thread-safe。嚴格地說,WeakPtr 可以跨 thread 執行,但不能同時在多個 thread 存取它。要仔細說明的話,得了解 Chromium 定的一些 API [*1],這裡就略過了。

以型別 T 的物件使用 WeakPtrFactory 為例,下面是 WeakPtr 的類別關聯圖。

比較

Chromium 的設計和 Java 以及 C++11 std 不同,需要 weak pointer 的 class 要在定義 class 時就作好配套措施,又需留意 thread safety。但是不會另外生出 shared pointer,效率比較好。

一般來說,有 thread safety 的問題就得用 lock 保護,因此會降低效率。不過 Chromium 許多物件的設計是限制在同一個 thread 上執行,設計上就不允許跨 thread 執行。所以 WeakPtr 這樣設計到也不會太突兀。

備註

  1. WeakPtr 要在同一個 SequenceTaskRunner 上執行,SequenceTaskRunner 底層實作可能是 single thread task runner,或是 thread pool。若是後者,post task 時有符合一些規範,SequenceTaskRunner 可以保證各個 task 依序執行 (可能在不同 thread 執行),就不會有兩個 task 同時在不同 thread 被執行。關於 SequenceTaskRunner,見這裡這裡的說明。