<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

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

          共 6932字,需瀏覽 14分鐘

           ·

          2021-10-14 01:08

          劇照 |?《基地》

          背景

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

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

          #?占用大量資源,創(chuàng)建銷(xiāo)毀成本很高
          class?Data:
          ????def?__init__(self,?key):
          ????????pass

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

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

          import?threading
          #?數(shù)據(jù)緩存
          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 內(nèi)部用一個(gè) dict 對(duì)象來(lái)緩存已創(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í)訪問(wèn),使用的都是緩存中的同一個(gè)副本。

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

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

          我們可以在 Cacher 中維護(hù)數(shù)據(jù)的引用次數(shù), get 方法自動(dòng)累加這個(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ù)對(duì)象,因此理論上不應(yīng)該對(duì)數(shù)據(jù)產(chǎn)生引用。那有什么黑科技能夠在不產(chǎn)生引用的前提下,找到目標(biāo)對(duì)象嗎?我們知道,賦值都是會(huì)產(chǎn)生引用的!

          典型用法

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

          #?創(chuàng)建一個(gè)數(shù)據(jù)
          >>>?d?=?Data('fasionchan.com')
          >>>?d
          <__main__.Data?object?at?0x1018571f0>

          #?創(chuàng)建一個(gè)指向該數(shù)據(jù)的弱引用
          >>>?import?weakref
          >>>?r?=?weakref.ref(d)

          #?調(diào)用弱引用對(duì)象,即可找到指向的對(duì)象
          >>>?r()
          <__main__.Data?object?at?0x1018571f0>
          >>>?r()?is?d
          True

          #?刪除臨時(shí)變量d,Data對(duì)象就沒(méi)有其他引用了,它將被回收
          >>>?del?d
          #?再次調(diào)用弱引用對(duì)象,發(fā)現(xiàn)目標(biāo)Data對(duì)象已經(jīng)不在了(返回None)
          >>>?r()

          這樣一來(lái),我們只需將 Cacher 緩存字典改成保存弱引用,問(wèn)題便迎刃而解!

          import?threading
          import?weakref
          #?數(shù)據(jù)緩存
          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 對(duì)象的弱引用,因此 Cacher 不會(huì)影響 Data 對(duì)象的引用計(jì)數(shù)。當(dāng)所有線程都用完數(shù)據(jù)后,引用計(jì)數(shù)就降為零因而被釋放。

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

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

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

          import?threading
          import?weakref
          #?數(shù)據(jù)緩存
          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 模塊還有很多好用的工具類(lèi)和工具函數(shù),具體細(xì)節(jié)請(qǐng)參考官方文檔,這里不再贅述。

          工作原理

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

          >>>?d?=?Data('fasionchan.com')

          #?weakref.ref?是一個(gè)內(nèi)置類(lèi)型對(duì)象
          >>>?from?weakref?import?ref
          >>>?ref
          <class?'weakref'>

          #?調(diào)用weakref.ref類(lèi)型對(duì)象,創(chuàng)建了一個(gè)弱引用實(shí)例對(duì)象
          >>>?r?=?ref(d)
          >>>?r
          <weakref?at?0x1008d5b80;?to?'Data'?at?0x100873d60>

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

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

          我們先扒一扒弱引用對(duì)象的字段結(jié)構(gòu),定義于 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

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

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

          結(jié)合代碼中的注釋?zhuān)覀冎溃?/p>

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

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

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

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

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

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

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

          /*?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 用于判斷類(lèi)型對(duì)象是否支持弱引用,僅當(dāng) tp_weaklistoffset 大于零才支持弱引用,內(nèi)置對(duì)象 list 等都不支持弱引用;
          • PyObject_GET_WEAKREFS_LISTPTR 用于取出一個(gè)對(duì)象的弱引用鏈表頭,它先通過(guò) Py_TYPE 宏找到類(lèi)型對(duì)象 t ,再找通過(guò) tp_weaklistoffset 字段確定偏移量,最后與對(duì)象地址相加即可得到鏈表頭字段的地址;

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

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

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

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

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

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

          Python貓技術(shù)交流群開(kāi)放啦!群里既有國(guó)內(nèi)一二線大廠在職員工,也有國(guó)內(nèi)外高校在讀學(xué)生,既有十多年碼齡的編程老鳥(niǎo),也有中小學(xué)剛剛?cè)腴T(mén)的新人,學(xué)習(xí)氛圍良好!想入群的同學(xué),請(qǐng)?jiān)诠?hào)內(nèi)回復(fù)『交流群』,獲取貓哥的微信(謝絕廣告黨,非誠(chéng)勿擾!)~


          還不過(guò)癮?試試它們




          Python 垃圾回收機(jī)制與原理解析

          如何評(píng)價(jià)說(shuō) Python 是最快的語(yǔ)言?

          Python 神器 Celery 源碼閱讀(1)

          Python 中最常用的 5 種線程鎖你會(huì)用嗎?

          Python 協(xié)程與 JavaScript 協(xié)程的對(duì)比

          冗長(zhǎng)的 Python 代碼,如何重構(gòu)?


          如果你覺(jué)得本文有幫助
          請(qǐng)慷慨分享點(diǎn)贊,感謝啦
          瀏覽 53
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  好吊操色| 精品国产一区二区三区四区 | 激情白浆 | 精品久久大香蕉 | 精品视频在线视频 |