網站首頁 編程語言 正文
背景
開始討論弱引用(?weakref?)之前,我們先來看看什么是弱引用?它到底有什么作用?
假設我們有一個多線程程序,并發處理應用數據:
# 占用大量資源,創建銷毀成本很高\
class Data:\
def __init__(self, key):\
pass
應用數據 Data 由一個 key 唯一標識,同一個數據可能被多個線程同時訪問。由于 Data 需要占用很多系統資源,創建和消費的成本很高。我們希望 Data 在程序中只維護一個副本,就算被多個線程同時訪問,也不想重復創建。
為此,我們嘗試設計一個緩存中間件 Cacher :
import threading
# 數據緩存
class Cacher:
def __init__(self):
self.pool = {}
self.lock = threading.Lock()
def get(self, key):
with self.lock:
data = self.pool.get(key)
if data:
return data
self.pool[key] = data = Data(key)
return data
Cacher 內部用一個 dict 對象來緩存已創建的 Data 副本,并提供 get 方法用于獲取應用數據 Data 。get 方法獲取數據時先查緩存字典,如果數據已存在,便直接將其返回;如果數據不存在,則創建一個并保存到字典中。因此,數據首次被創建后就進入緩存字典,后續如有其它線程同時訪問,使用的都是緩存中的同一個副本。
感覺非常不錯!但美中不足的是:Cacher 有資源泄露的風險!
因為 Data 一旦被創建后,就保存在緩存字典中,永遠都不會釋放!換句話講,程序的資源比如內存,會不斷地增長,最終很有可能會爆掉。因此,我們希望一個數據等所有線程都不再訪問后,能夠自動釋放。
我們可以在 Cacher 中維護數據的引用次數,?get?方法自動累加這個計數。于此同時提供一個?remove?新方法用于釋放數據,它先自減引用次數,并在引用次數降為零時將數據從緩存字段中刪除。
線程調用?get?方法獲取數據,數據用完后需要調用 remove 方法將其釋放。Cacher 相當于自己也實現了一遍引用計數法,這也太麻煩了吧!Python 不是內置了垃圾回收機制嗎?為什么應用程序還需要自行實現呢?
沖突的主要癥結在于 Cacher 的緩存字典:它作為一個中間件,本身并不使用數據對象,因此理論上不應該對數據產生引用。那有什么黑科技能夠在不產生引用的前提下,找到目標對象嗎?我們知道,賦值都是會產生引用的!
典型用法
這時,弱引用(?weakref?)隆重登場了!弱引用是一種特殊的對象,能夠在不產生引用的前提下,關聯目標對象。
# 創建一個數據
>>> d = Data('fasionchan.com')
>>> d
<__main__.Data object at 0x1018571f0>
# 創建一個指向該數據的弱引用
>>> import weakref
>>> r = weakref.ref(d)
# 調用弱引用對象,即可找到指向的對象
>>> r()
<__main__.Data object at 0x1018571f0>
>>> r() is d
True
# 刪除臨時變量d,Data對象就沒有其他引用了,它將被回收
>>> del d
# 再次調用弱引用對象,發現目標Data對象已經不在了(返回None)
>>> r()
這樣一來,我們只需將 Cacher 緩存字典改成保存弱引用,問題便迎刃而解!
import threading
import weakref
# 數據緩存
class Cacher:
def __init__(self):
self.pool = {}
self.lock = threading.Lock()
def get(self, key):
with self.lock:
r = self.pool.get(key)
if r:
data = r()
if data:
return data
data = Data(key)
self.pool[key] = weakref.ref(data)
return data
由于緩存字典只保存 Data 對象的弱引用,因此 Cacher 不會影響 Data 對象的引用計數。當所有線程都用完數據后,引用計數就降為零因而被釋放。
實際上,用字典緩存數據對象的做法很常用,為此?weakref?模塊還提供了兩種只保存弱引用的字典對象:
- weakref.WeakKeyDictionary?,鍵只保存弱引用的映射類(一旦鍵不再有強引用,鍵值對條目將自動消失);
- weakref.WeakValueDictionary?,值只保存弱引用的映射類(一旦值不再有強引用,鍵值對條目將自動消失);
因此,我們的數據緩存字典可以采用?weakref.WeakValueDictionary?來實現,它的接口跟普通字典完全一樣。這樣我們不用再自行維護弱引用對象,代碼邏輯更加簡潔明了:
import threading
import weakref
# 數據緩存
class Cacher:
def __init__(self):
self.pool = weakref.WeakValueDictionary()
self.lock = threading.Lock()
def get(self, key):
with self.lock:
data = self.pool.get(key)
if data:
return data
self.pool[key] = data = Data(key)
return data
weakref?模塊還有很多好用的工具類和工具函數,具體細節請參考官方文檔,這里不再贅述。
工作原理
那么,弱引用到底是何方神圣,為什么會有如此神奇的魔力呢?接下來,我們一起揭下它的面紗,一睹真容!
>>> d = Data('fasionchan.com')
# weakref.ref 是一個內置類型對象
>>> from weakref import ref
>>> ref
<class 'weakref'>
# 調用weakref.ref類型對象,創建了一個弱引用實例對象
>>> r = ref(d)
>>> r
<weakref at 0x1008d5b80; to 'Data' at 0x100873d60>
經過前面章節,我們對閱讀內建對象源碼已經輕車熟路了,相關源碼文件如下:
- Include/weakrefobject.h?頭文件包含對象結構體和一些宏定義;
- Objects/weakrefobject.c?源文件包含弱引用類型對象及其方法定義;
我們先扒一扒弱引用對象的字段結構,定義于?Include/weakrefobject.h?頭文件中的第?10-41?行:
typedef struct _PyWeakReference PyWeakReference;
/* PyWeakReference is the base struct for the Python ReferenceType, ProxyType,
* and CallableProxyType.
*/
#ifndef Py_LIMITED_API
struct _PyWeakReference {
PyObject_HEAD
/* The object to which this is a weak reference, or Py_None if none.
* Note that this is a stealth reference: wr_object's refcount is
* not incremented to reflect this pointer.
*/
PyObject *wr_object;
/* A callable to invoke when wr_object dies, or NULL if none. */
PyObject *wr_callback;
/* A cache for wr_object's hash code. As usual for hashes, this is -1
* if the hash code isn't known yet.
*/
Py_hash_t hash;
/* If wr_object is weakly referenced, wr_object has a doubly-linked NULL-
* terminated list of weak references to it. These are the list pointers.
* If wr_object goes away, wr_object is set to Py_None, and these pointers
* have no meaning then.
*/
PyWeakReference *wr_prev;
PyWeakReference *wr_next;
};
#endif
由此可見,PyWeakReference?結構體便是弱引用對象的肉身。它是一個定長對象,除固定頭部外還有?5?個字段:
- wr_object?,對象指針,指向被引用對象,弱引用根據該字段可以找到被引用對象,但不會產生引用;
- wr_callback?,指向一個可調用對象,當被引用的對象銷毀時將被調用;
- hash?,緩存被引用對象的哈希值;
- wr_prev?和?wr_next?分別是前后向指針,用于將弱引用對象組織成雙向鏈表;
結合代碼中的注釋,我們知道:
- 弱引用對象通過?wr_object?字段關聯被引用的對象,如上圖虛線箭頭所示;
- 一個對象可以同時被多個弱引用對象關聯,圖中的?Data?實例對象被兩個弱引用對象關聯;
- 所有關聯同一個對象的弱引用,被組織成一個雙向鏈表,鏈表頭保存在被引用對象中,如上圖實線箭頭所示;
- 當一個對象被銷毀后,Python 將遍歷它的弱引用鏈表,逐一處理:
- 將 wr_object 字段設為 None ,弱引用對象再被調用將返回 None ,調用者便知道對象已經被銷毀了;
- 執行回調函數?wr_callback?(如有);
由此可見,弱引用的工作原理其實就是設計模式中的?觀察者模式(?Observer?)。當對象被銷毀,它的所有弱引用對象都得到通知,并被妥善處理。
實現細節
掌握弱引用的基本原理,足以讓我們將其用好。如果您對源碼感興趣,還可以再深入研究它的一些實現細節。
前面我們提到,對同一對象的所有弱引用,被組織成一個雙向鏈表,鏈表頭保存在對象中。由于能夠創建弱引用的對象類型是多種多樣的,很難由一個固定的結構體來表示。因此,Python 在類型對象中提供一個字段 tp_weaklistoffset ,記錄弱引用鏈表頭指針在實例對象中的偏移量。
由此一來,對于任意對象 o ,我們只需通過 ob_type 字段找到它的類型對象 t ,再根據 t 中的 tp_weaklistoffset 字段即可找到對象 o 的弱引用鏈表頭。
Python 在?Include/objimpl.h?頭文件中提供了兩個宏定義:
/* Test if a type supports weak references */
#define PyType_SUPPORTS_WEAKREFS(t) ((t)->tp_weaklistoffset > 0)
#define PyObject_GET_WEAKREFS_LISTPTR(o) \
((PyObject **) (((char *) (o)) + Py_TYPE(o)->tp_weaklistoffset))
- PyType_SUPPORTS_WEAKREFS 用于判斷類型對象是否支持弱引用,僅當 tp_weaklistoffset 大于零才支持弱引用,內置對象 list 等都不支持弱引用;
- PyObject_GET_WEAKREFS_LISTPTR 用于取出一個對象的弱引用鏈表頭,它先通過 Py_TYPE 宏找到類型對象 t ,再找通過 tp_weaklistoffset 字段確定偏移量,最后與對象地址相加即可得到鏈表頭字段的地址;
我們創建弱引用時,需要調用弱引用類型對象?weakref?并將被引用對象 d 作為參數傳進去。弱引用類型對象?weakref?是所有弱引用實例對象的類型,是一個全局唯一的類型對象,定義在?Objects/weakrefobject.c?中,即:_PyWeakref_RefType(第 350 行)。
根據對象模型中學到的知識,Python?調用一個對象時,執行的是其類型對象中的?tp_call?函數。因此,調用弱引用類型對象?weakref?時,執行的是?weakref?的類型對象,也就是?type?的?tp_call?函數。tp_call 函數則回過頭來調用 weakref 的 tp_new 和 tp_init 函數,其中 tp_new 為實例對象分配內存,而 tp_init 則負責初始化實例對象。
回到?Objects/weakrefobject.c?源文件,可以看到 PyWeakref_RefType 的 tp_new 字段被初始化成?*weakref___new_* ?(第?276?行)。該函數的主要處理邏輯如下:
- 解析參數,得到被引用的對象(第 282 行);
- 調用?PyType_SUPPORTS_WEAKREFS?宏判斷被引用的對象是否支持弱引用,不支持就拋異常(第 286 行);
- 調用?GET_WEAKREFS_LISTPTR?行取出對象的弱引用鏈表頭字段,為方便插入返回的是一個二級指針(第 294 行);
- 調用 get_basic_refs 取出鏈表最前那個 callback 為空?基礎弱引用對象(如有,第 295 行);
- 如果?callback?為空,而且對象存在?callback?為空的基礎弱引用,則復用該實例直接將其返回(第 296 行);
- 如果不能復用,調用 tp_alloc 函數分配內存、完成字段初始化,并插到對象的弱引用鏈表(第 309 行);
- 如果 callback 為空,直接將其插入到鏈表最前面,方便后續復用(見第 4 點);
- 如果 callback 非空,將其插到基礎弱引用對象(如有)之后,保證基礎弱引用位于鏈表頭,方便獲取;
當一個對象被回收后,tp_dealloc 函數將調用?PyObject_ClearWeakRefs?函數對它的弱引用進行清理。該函數取出對象的弱引用鏈表,然后逐個遍歷,清理?wr_object?字段并執行?wr_callback?回調函數(如有)。具體細節不再展開,有興趣的話可以自行查閱?Objects/weakrefobject.c?中的源碼,位于?880?行。
好了,經過本節學習,我們徹底掌握了弱引用相關知識。弱引用可以在不產生引用計數的前提下,對目標對象進行管理,常用于框架和中間件中。弱引用看起來很神奇,其實設計原理是非常簡單的觀察者模式。弱引用對象創建后便插到一個由目標對象維護的鏈表中,觀察(訂閱)對象的銷毀事件。
總結
原文鏈接:https://juejin.cn/post/7072729068516409358
相關推薦
- 2022-04-12 python入門之scrapy框架中Request對象和Response對象的介紹_python
- 2022-11-23 Python?property裝飾器使用案例介紹_python
- 2022-03-16 C#?程序通用結構_C#教程
- 2022-06-09 使用Ajax實現進度條的繪制_AJAX相關
- 2022-05-20 python?關鍵字與標識符超詳細整理_python
- 2022-03-17 Redis快速部署為Docker容器的實現方法_docker
- 2022-04-26 SQL將一個表中的數據插入到另一個表中的方法_MsSql
- 2023-04-03 gin項目部署到服務器并后臺啟動的步驟_Golang
- 最近更新
-
- window11 系統安裝 yarn
- 超詳細win安裝深度學習環境2025年最新版(
- Linux 中運行的top命令 怎么退出?
- MySQL 中decimal 的用法? 存儲小
- get 、set 、toString 方法的使
- @Resource和 @Autowired注解
- Java基礎操作-- 運算符,流程控制 Flo
- 1. Int 和Integer 的區別,Jav
- spring @retryable不生效的一種
- Spring Security之認證信息的處理
- Spring Security之認證過濾器
- Spring Security概述快速入門
- Spring Security之配置體系
- 【SpringBoot】SpringCache
- Spring Security之基于方法配置權
- redisson分布式鎖中waittime的設
- maven:解決release錯誤:Artif
- restTemplate使用總結
- Spring Security之安全異常處理
- MybatisPlus優雅實現加密?
- Spring ioc容器與Bean的生命周期。
- 【探索SpringCloud】服務發現-Nac
- Spring Security之基于HttpR
- Redis 底層數據結構-簡單動態字符串(SD
- arthas操作spring被代理目標對象命令
- Spring中的單例模式應用詳解
- 聊聊消息隊列,發送消息的4種方式
- bootspring第三方資源配置管理
- GIT同步修改后的遠程分支