自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

Python 中弱引用的神奇用法與原理探析

開發(fā) 后端
弱引用可以在不產(chǎn)生引用計(jì)數(shù)的前提下,對目標(biāo)對象進(jìn)行管理,常用于框架和中間件中。弱引用看起來很神奇,其實(shí)設(shè)計(jì)原理是非常簡單的觀察者模式。

[[439622]]

 背景

開始討論弱引用( weakref )之前,我們先來看看什么是弱引用?它到底有什么作用?

假設(shè)我們有一個(gè)多線程程序,并發(fā)處理應(yīng)用數(shù)據(jù): 

  1. # 占用大量資源,創(chuàng)建銷毀成本很高  
  2. class Data:  
  3.     def __init__(self, key):  
  4.         pass 

應(yīng)用數(shù)據(jù) Data 由一個(gè) key 唯一標(biāo)識,同一個(gè)數(shù)據(jù)可能被多個(gè)線程同時(shí)訪問。由于 Data 需要占用很多系統(tǒng)資源,創(chuàng)建和消費(fèi)的成本很高。我們希望 Data 在程序中只維護(hù)一個(gè)副本,就算被多個(gè)線程同時(shí)訪問,也不想重復(fù)創(chuàng)建。

為此,我們嘗試設(shè)計(jì)一個(gè)緩存中間件 Cacher : 

  1. import threading  
  2. # 數(shù)據(jù)緩存  
  3. class Cacher:  
  4.     def __init__(self):  
  5.         self.pool = {}  
  6.         self.lock = threading.Lock()  
  7.     def get(self, key):  
  8.         with self.lock:  
  9.             data = self.pool.get(key)  
  10.             if data:  
  11.                 return data  
  12.             self.pool[key] = data = Data(key)  
  13.             return data 

Cacher 內(nèi)部用一個(gè) dict 對象來緩存已創(chuàng)建的 Data 副本,并提供 get 方法用于獲取應(yīng)用數(shù)據(jù) Data 。get 方法獲取數(shù)據(jù)時(shí)先查緩存字典,如果數(shù)據(jù)已存在,便直接將其返回;如果數(shù)據(jù)不存在,則創(chuàng)建一個(gè)并保存到字典中。因此,數(shù)據(jù)首次被創(chuàng)建后就進(jìn)入緩存字典,后續(xù)如有其它線程同時(shí)訪問,使用的都是緩存中的同一個(gè)副本。

感覺非常不錯(cuò)!但美中不足的是:Cacher 有資源泄露的風(fēng)險(xiǎn)!

因?yàn)?Data 一旦被創(chuàng)建后,就保存在緩存字典中,永遠(yuǎn)都不會釋放!換句話講,程序的資源比如內(nèi)存,會不斷地增長,最終很有可能會爆掉。因此,我們希望一個(gè)數(shù)據(jù)等所有線程都不再訪問后,能夠自動釋放。

我們可以在 Cacher 中維護(hù)數(shù)據(jù)的引用次數(shù), get 方法自動累加這個(gè)計(jì)數(shù)。于此同時(shí)提供一個(gè) remove 新方法用于釋放數(shù)據(jù),它先自減引用次數(shù),并在引用次數(shù)降為零時(shí)將數(shù)據(jù)從緩存字段中刪除。

線程調(diào)用 get 方法獲取數(shù)據(jù),數(shù)據(jù)用完后需要調(diào)用 remove 方法將其釋放。Cacher 相當(dāng)于自己也實(shí)現(xiàn)了一遍引用計(jì)數(shù)法,這也太麻煩了吧!Python 不是內(nèi)置了垃圾回收機(jī)制嗎?為什么應(yīng)用程序還需要自行實(shí)現(xiàn)呢?

沖突的主要癥結(jié)在于 Cacher 的緩存字典:它作為一個(gè)中間件,本身并不使用數(shù)據(jù)對象,因此理論上不應(yīng)該對數(shù)據(jù)產(chǎn)生引用。那有什么黑科技能夠在不產(chǎn)生引用的前提下,找到目標(biāo)對象嗎?我們知道,賦值都是會產(chǎn)生引用的!

典型用法

這時(shí),弱引用( weakref )隆重登場了!弱引用是一種特殊的對象,能夠在不產(chǎn)生引用的前提下,關(guān)聯(lián)目標(biāo)對象。 

  1. # 創(chuàng)建一個(gè)數(shù)據(jù)  
  2. >>> d = Data('fasionchan.com')  
  3. >>> d  
  4. <__main__.Data object at 0x1018571f0>  
  5. # 創(chuàng)建一個(gè)指向該數(shù)據(jù)的弱引用  
  6. >>> import weakref  
  7. >>> r = weakref.ref(d)  
  8. # 調(diào)用弱引用對象,即可找到指向的對象  
  9. >>> r()  
  10. <__main__.Data object at 0x1018571f0>  
  11. >>> r() is d  
  12. True  
  13. # 刪除臨時(shí)變量d,Data對象就沒有其他引用了,它將被回收  
  14. >>> del d  
  15. # 再次調(diào)用弱引用對象,發(fā)現(xiàn)目標(biāo)Data對象已經(jīng)不在了(返回None)  
  16. >>> r() 

這樣一來,我們只需將 Cacher 緩存字典改成保存弱引用,問題便迎刃而解! 

  1. import threading  
  2. import weakref  
  3. # 數(shù)據(jù)緩存  
  4. class Cacher:  
  5.     def __init__(self):  
  6.         self.pool = {}  
  7.         self.lock = threading.Lock()  
  8.     def get(self, key):  
  9.         with self.lock:  
  10.             r = self.pool.get(key)  
  11.             if r:  
  12.                 data = r()  
  13.                 if data:  
  14.                     return data  
  15.             data = Data(key)  
  16.             self.pool[key] = weakref.ref(data)  
  17.             return data 

由于緩存字典只保存 Data 對象的弱引用,因此 Cacher 不會影響 Data 對象的引用計(jì)數(shù)。當(dāng)所有線程都用完數(shù)據(jù)后,引用計(jì)數(shù)就降為零因而被釋放。

實(shí)際上,用字典緩存數(shù)據(jù)對象的做法很常用,為此 weakref 模塊還提供了兩種只保存弱引用的字典對象:

  •  weakref.WeakKeyDictionary ,鍵只保存弱引用的映射類(一旦鍵不再有強(qiáng)引用,鍵值對條目將自動消失);
  •  weakref.WeakValueDictionary ,值只保存弱引用的映射類(一旦值不再有強(qiáng)引用,鍵值對條目將自動消失);

因此,我們的數(shù)據(jù)緩存字典可以采用 weakref.WeakValueDictionary 來實(shí)現(xiàn),它的接口跟普通字典完全一樣。這樣我們不用再自行維護(hù)弱引用對象,代碼邏輯更加簡潔明了: 

  1. import threading  
  2. import weakref  
  3. # 數(shù)據(jù)緩存  
  4. class Cacher:  
  5.     def __init__(self):  
  6.         self.pool = weakref.WeakValueDictionary()  
  7.         self.lock = threading.Lock()  
  8.     def get(self, key):  
  9.         with self.lock:  
  10.             data = self.pool.get(key)  
  11.             if data:  
  12.                 return data  
  13.             self.pool[key] = data = Data(key)  
  14.             return data 

weakref 模塊還有很多好用的工具類和工具函數(shù),具體細(xì)節(jié)請參考官方文檔,這里不再贅述。

工作原理

那么,弱引用到底是何方神圣,為什么會有如此神奇的魔力呢?接下來,我們一起揭下它的面紗,一睹真容! 

  1. >>> d = Data('fasionchan.com')  
  2. # weakref.ref 是一個(gè)內(nèi)置類型對象  
  3. >>> from weakref import ref  
  4. >>> ref  
  5. <class 'weakref'>  
  6. # 調(diào)用weakref.ref類型對象,創(chuàng)建了一個(gè)弱引用實(shí)例對象  
  7. >>> r = ref(d)  
  8. >>> r  
  9. <weakref at 0x1008d5b80; to 'Data' at 0x100873d60>  

經(jīng)過前面章節(jié),我們對閱讀內(nèi)建對象源碼已經(jīng)輕車熟路了,相關(guān)源碼文件如下:

  •  Include/weakrefobject.h 頭文件包含對象結(jié)構(gòu)體和一些宏定義;
  •  Objects/weakrefobject.c 源文件包含弱引用類型對象及其方法定義;

我們先扒一扒弱引用對象的字段結(jié)構(gòu),定義于 Include/weakrefobject.h 頭文件中的第 10-41 行: 

  1. typedef struct _PyWeakReference PyWeakReference;  
  2. /* PyWeakReference is the base struct for the Python ReferenceType, ProxyType,  
  3.  * and CallableProxyType.  
  4.  */  
  5. #ifndef Py_LIMITED_API  
  6. struct _PyWeakReference {  
  7.     PyObject_HEAD  
  8.     /* The object to which this is a weak reference, or Py_None if none.  
  9.      * Note that this is a stealth reference:  wr_object's refcount is  
  10.      * not incremented to reflect this pointer.  
  11.      */  
  12.     PyObject *wr_object;  
  13.     /* A callable to invoke when wr_object dies, or NULL if none. */  
  14.     PyObject *wr_callback;  
  15.     /* A cache for wr_object's hash code.  As usual for hashes, this is -1  
  16.      * if the hash code isn't known yet.  
  17.      */  
  18.     Py_hash_t hash;  
  19.     /* If wr_object is weakly referenced, wr_object has a doubly-linked NULL-  
  20.      * terminated list of weak references to it.  These are the list pointers.  
  21.      * If wr_object goes away, wr_object is set to Py_None, and these pointers 
  22.       * have no meaning then.  
  23.      */ 
  24.      PyWeakReference *wr_prev;  
  25.     PyWeakReference *wr_next;  
  26. };  
  27. #endif 

由此可見,PyWeakReference 結(jié)構(gòu)體便是弱引用對象的肉身。它是一個(gè)定長對象,除固定頭部外還有 5 個(gè)字段:

  •  wr_object ,對象指針,指向被引用對象,弱引用根據(jù)該字段可以找到被引用對象,但不會產(chǎn)生引用;
  •  wr_callback ,指向一個(gè)可調(diào)用對象,當(dāng)被引用的對象銷毀時(shí)將被調(diào)用;
  •  hash ,緩存被引用對象的哈希值;
  •  wr_prev 和 wr_next 分別是前后向指針,用于將弱引用對象組織成雙向鏈表;

結(jié)合代碼中的注釋,我們知道:

  •  弱引用對象通過 wr_object 字段關(guān)聯(lián)被引用的對象,如上圖虛線箭頭所示;
  •  一個(gè)對象可以同時(shí)被多個(gè)弱引用對象關(guān)聯(lián),圖中的 Data 實(shí)例對象被兩個(gè)弱引用對象關(guān)聯(lián);
  •  所有關(guān)聯(lián)同一個(gè)對象的弱引用,被組織成一個(gè)雙向鏈表,鏈表頭保存在被引用對象中,如上圖實(shí)線箭頭所示;
  •  當(dāng)一個(gè)對象被銷毀后,Python 將遍歷它的弱引用鏈表,逐一處理:
    •   將 wr_object 字段設(shè)為 None ,弱引用對象再被調(diào)用將返回 None ,調(diào)用者便知道對象已經(jīng)被銷毀了;
    •   執(zhí)行回調(diào)函數(shù) wr_callback (如有);

由此可見,弱引用的工作原理其實(shí)就是設(shè)計(jì)模式中的 觀察者模式( Observer )。當(dāng)對象被銷毀,它的所有弱引用對象都得到通知,并被妥善處理。

實(shí)現(xiàn)細(xì)節(jié)

掌握弱引用的基本原理,足以讓我們將其用好。如果您對源碼感興趣,還可以再深入研究它的一些實(shí)現(xiàn)細(xì)節(jié)。

前面我們提到,對同一對象的所有弱引用,被組織成一個(gè)雙向鏈表,鏈表頭保存在對象中。由于能夠創(chuàng)建弱引用的對象類型是多種多樣的,很難由一個(gè)固定的結(jié)構(gòu)體來表示。因此,Python 在類型對象中提供一個(gè)字段 tp_weaklistoffset ,記錄弱引用鏈表頭指針在實(shí)例對象中的偏移量。

由此一來,對于任意對象 o ,我們只需通過 ob_type 字段找到它的類型對象 t ,再根據(jù) t 中的 tp_weaklistoffset 字段即可找到對象 o 的弱引用鏈表頭。

Python 在 Include/objimpl.h 頭文件中提供了兩個(gè)宏定義: 

  1. /* Test if a type supports weak references */  
  2. #define PyType_SUPPORTS_WEAKREFS(t) ((t)->tp_weaklistoffset > 0)  
  3. #define PyObject_GET_WEAKREFS_LISTPTR(o) \  
  4.     ((PyObject **) (((char *) (o)) + Py_TYPE(o)->tp_weaklistoffset)) 
  •  PyType_SUPPORTS_WEAKREFS 用于判斷類型對象是否支持弱引用,僅當(dāng) tp_weaklistoffset 大于零才支持弱引用,內(nèi)置對象 list 等都不支持弱引用;
  •  PyObject_GET_WEAKREFS_LISTPTR 用于取出一個(gè)對象的弱引用鏈表頭,它先通過 Py_TYPE 宏找到類型對象 t ,再找通過 tp_weaklistoffset 字段確定偏移量,最后與對象地址相加即可得到鏈表頭字段的地址;

我們創(chuàng)建弱引用時(shí),需要調(diào)用弱引用類型對象 weakref 并將被引用對象 d 作為參數(shù)傳進(jìn)去。弱引用類型對象 weakref 是所有弱引用實(shí)例對象的類型,是一個(gè)全局唯一的類型對象,定義在 Objects/weakrefobject.c 中,即:_PyWeakref_RefType(第 350 行)。

根據(jù)對象模型中學(xué)到的知識,Python 調(diào)用一個(gè)對象時(shí),執(zhí)行的是其類型對象中的 tp_call 函數(shù)。因此,調(diào)用弱引用類型對象 weakref 時(shí),執(zhí)行的是 weakref 的類型對象,也就是 type 的 tp_call 函數(shù)。tp_call 函數(shù)則回過頭來調(diào)用 weakref 的 tp_new 和 tp_init 函數(shù),其中 tp_new 為實(shí)例對象分配內(nèi)存,而 tp_init 則負(fù)責(zé)初始化實(shí)例對象。

回到 Objects/weakrefobject.c 源文件,可以看到 _PyWeakref_RefType 的 tp_new 字段被初始化成 weakref___new__ (第 276 行)。該函數(shù)的主要處理邏輯如下:

  1.  解析參數(shù),得到被引用的對象(第 282 行);
  2.  調(diào)用 PyType_SUPPORTS_WEAKREFS 宏判斷被引用的對象是否支持弱引用,不支持就拋異常(第 286 行);
  3.  調(diào)用 GET_WEAKREFS_LISTPTR 行取出對象的弱引用鏈表頭字段,為方便插入返回的是一個(gè)二級指針(第 294 行);
  4.  調(diào)用 get_basic_refs 取出鏈表最前那個(gè) callback 為空 基礎(chǔ)弱引用對象(如有,第 295 行);
  5.  如果 callback 為空,而且對象存在 callback 為空的基礎(chǔ)弱引用,則復(fù)用該實(shí)例直接將其返回(第 296 行);
  6.  如果不能復(fù)用,調(diào)用 tp_alloc 函數(shù)分配內(nèi)存、完成字段初始化,并插到對象的弱引用鏈表(第 309 行);
    •   如果 callback 為空,直接將其插入到鏈表最前面,方便后續(xù)復(fù)用(見第 4 點(diǎn));
    •   如果 callback 非空,將其插到基礎(chǔ)弱引用對象(如有)之后,保證基礎(chǔ)弱引用位于鏈表頭,方便獲取;

當(dāng)一個(gè)對象被回收后,tp_dealloc 函數(shù)將調(diào)用 PyObject_ClearWeakRefs 函數(shù)對它的弱引用進(jìn)行清理。該函數(shù)取出對象的弱引用鏈表,然后逐個(gè)遍歷,清理 wr_object 字段并執(zhí)行 wr_callback 回調(diào)函數(shù)(如有)。具體細(xì)節(jié)不再展開,有興趣的話可以自行查閱 Objects/weakrefobject.c 中的源碼,位于 880 行。

好了,經(jīng)過本節(jié)學(xué)習(xí),我們徹底掌握了弱引用相關(guān)知識。弱引用可以在不產(chǎn)生引用計(jì)數(shù)的前提下,對目標(biāo)對象進(jìn)行管理,常用于框架和中間件中。弱引用看起來很神奇,其實(shí)設(shè)計(jì)原理是非常簡單的觀察者模式。弱引用對象創(chuàng)建后便插到一個(gè)由目標(biāo)對象維護(hù)的鏈表中,觀察(訂閱)對象的銷毀事件。 

 

責(zé)任編輯:龐桂玉 來源: Python開發(fā)者
相關(guān)推薦

2021-10-08 21:00:52

數(shù)據(jù)弱引用對象

2013-08-19 17:14:04

.Net強(qiáng)引用弱引用

2015-11-02 17:20:00

Java弱引用

2021-01-07 14:20:55

JavaGC

2020-11-11 08:55:32

SparkJava磁盤

2020-12-02 09:01:40

Java基礎(chǔ)

2021-10-18 15:50:49

Android強(qiáng)引用軟引用

2020-02-09 17:23:17

Python數(shù)據(jù)字典

2024-04-01 00:05:00

ChatGPTSSE

2024-05-20 08:58:13

Java引用類型垃圾回收器

2009-06-16 11:26:22

弱引用內(nèi)存泄露

2022-01-02 06:55:08

Node.js ObjectWrapAddon

2010-04-27 10:08:49

2024-06-11 14:57:00

2013-09-16 16:48:50

Android優(yōu)化軟引用

2016-08-24 15:39:46

ownCloud存儲服務(wù)器

2019-11-05 15:52:23

Java源碼分析垃圾回收

2024-03-13 07:53:57

弱引用線程工具

2009-06-19 16:19:23

Java對象引用

2024-06-18 08:14:21

點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號