擴展Python模塊系列(四)----引用計數問題的處理

来源:http://www.cnblogs.com/jianmu/archive/2017/09/02/7367698.html
-Advertisement-
Play Games

承接上文,發現在使用Python C/C++ API擴展Python模塊時,總要在各種各樣的地方考慮到引用計數問題,稍不留神可能會導致擴展的模塊存在記憶體泄漏。引用計數問題是C語言擴展Python模塊最頭疼的地方,需要由程式員對使用的每個C API都要充分瞭解,甚至要熟悉源碼才能精確掌握什麼時候引用計 ...


      承接上文,發現在使用Python C/C++ API擴展Python模塊時,總要在各種各樣的地方考慮到引用計數問題,稍不留神可能會導致擴展的模塊存在記憶體泄漏。引用計數問題是C語言擴展Python模塊最頭疼的地方,需要由程式員對使用的每個C API都要充分瞭解,甚至要熟悉源碼才能精確掌握什麼時候引用計數加一,什麼時候減一。

  本文為翻譯文章,我覺得對於源碼中的引用計數講解得比較清楚,所以就翻譯為中文。http://edcjones.tripod.com/refcount.html#

Summary:

 Python Object的結構體定義包含一個引用計數和對象類型:

#define PyObject_HEAD \
             int ob_refcnt; \
             struct _typeobject *ob_type;

     typedef struct _object {
             PyObject_HEAD
     } PyObject;

 

    Python提供了兩組與引用計數相關的巨集定義【object.h】: 

#define Py_INCREF(op) (                                 \
        _Py_CHECK_THREAD_SAVE                           \
        _Py_INC_REFTOTAL _Py_REF_DEBUG_COMMA            \
        ((PyObject*)(op))->ob_refcnt++)
/*當引用計數為0時,會釋放對象所占的記憶體*/
#define Py_DECREF(op)                                   \
    do {                                                \
        if (_Py_CHECK_THREAD_SAVE                       \
            _Py_DEC_REFTOTAL _Py_REF_DEBUG_COMMA        \
            --((PyObject*)(op))->ob_refcnt != 0)        \
            _Py_CHECK_REFCNT(op)                        \
        else                                            \
            _Py_Dealloc((PyObject *)(op));              \
    } while (0)

另外一組考慮是對象為NULl的情況:

#define Py_XINCREF(op) do { if ((op) == NULL) ; else Py_INCREF(op); } while (0)
#define Py_XDECREF(op) do { if ((op) == NULL) ; else Py_DECREF(op); } while (0)

 在Python中,沒有誰能真正擁有一個對象,只擁有對象的引用。一個對象的reference count定義為該對象的引用者數量,對象的引用者當不再使用該對象時有責任主動調用Py_DECREF(),當reference count為0時,Python可能會delete這個對象。

每次調用Py_INCREF(),最終都應該對應調用Py_DECREF()。C語言中,每個malloc,必須最終調用free()。而現實很容易忘記free掉在堆上分配的記憶體,而且不使用工具的話也難以察覺記憶體泄漏問題,因為現代機器記憶體、虛擬記憶體都很充足,一般會在長時間運行的伺服器程式上出現記憶體泄漏問題。

當一個指向Python Object的指針的引用計數加1,就是說這個對象被protected.

什麼時候調用Py_INCREF()和Py_DECREF()

從 函數中返回一個Python Object

大部分Python 對象是通過Python C API提供的函數來創建的,一般形如 PyObject* Py_Something(arguments)創建一個Python 對象,然後返回給調用者。一般在Py_Something函數中對該Python對象調用了Py_INCREF(並不是所有的函數都會調用),而調用Py_Something的函數在使用其返回的Python對象時要牢記該對象引用計數已被加1,當不再需要該對象時需要調用Py_DECREF()。

 

 void MyCode(arguments) {
         PyObject* pyo;
         ...
         pyo = Py_Something(args);

 

MyCode函數調用了Py_Something,有責任處理pyo的引用計數,當MyCode使用完了pyo之後,必須要調用Py_DECREF(pyo)。

不過,如果MyCode需要返回pyo對象,比如:

 PyObject* MyCode(arguments) {
         PyObject* pyo;
         ...
         pyo = Py_Something(args);
         ...
         return pyo;
     }

此時,MyCode不應該調用PY_DECREF(),在這種情況下,MyCode將pyo對象的引用計數責任傳遞了出去。

Note:如果一個函數返回的是None對象,C代碼應該是這樣:必須要增加None對象的引用計數。

    Py_INCREF(Py_None);
    return Py_None;

到目前為止討論了最常見的情況,即當調用Py_Something創建了一個引用,並將引用計數的責任傳遞給其調用者。在Python文檔中,這被稱為new reference。比如文檔中有說明:

  PyObject* PyList_New(int len)
           Return value: New reference.
           Returns a new list of length len on success, or NULL on failure.

當一個引用被INCREF,通常稱為這個引用被protected。

有時候,Python源碼中不會調用Py_DECREF()。 

  PyObject *
     PyTuple_GetItem(register PyObject *op, register int i)
     {
             if (!PyTuple_Check(op)) {
                     PyErr_BadInternalCall();
                     return NULL;
             }
             if (i < 0 || i >= ((PyTupleObject *)op) -> ob_size) {
                     PyErr_SetString(PyExc_IndexError,
                              "tuple index out of range");
                     return NULL;
             }
             return ((PyTupleObject *)op) -> ob_item[i];
     }

這種情況被稱為borrowing a reference。

 PyObject* PyTuple_GetItem(PyObject *p, int pos)
          Return value: Borrowed reference.
          Returns the object at position pos in the tuple pointed to by
          p.  If pos is out of bounds, returns NULL and sets an
          IndexError exception.

本文也稱之為這個對象的引用是unprotected。

Python源碼中,返回unprotected preferencess(borrowing a reference)的函數有:

PyTuple_GetItem(),
PyList_GetItem(),
PyList_GET_ITEM(),
PyList_SET_ITEM(),
PyDict_GetItem(),
PyDict_GetItemString(),
PyErr_Occurred(),
PyFile_Name(),
PyImport_GetModuleDict(),
PyModule_GetDict(),
PyImport_AddModule(),
PyObject_Init(),
Py_InitModule(),
Py_InitModule3(),
Py_InitModule4(), and
PySequence_Fast_GET_ITEM().

對於PyArg_ParseTuple()來說,這個函數有時候會返回PyObject,存在類型為PyObject*的參數中。比如sysmodule.c中的例子:

 static PyObject *
     sys_getrefcount(PyObject *self, PyObject *args)
     {
             PyObject *arg;
             if (!PyArg_ParseTuple(args, "O:getrefcount", &arg))
                     return NULL;
             return PyInt_FromLong(arg->ob_refcnt);
     }

PyArg_ParseTuple源碼的實現中,沒有對arg的引用計數INCREF,所以arg是一個unprotected object,當sys_getrefcount返回時,arg不應當被DECREF。

這裡提供一個比較完整的功能函數,計算一個列表中的整數之和.

示例1:

     long sum_list(PyObject *list)
     {
         int i, n;
         long total = 0;
         PyObject *item;

         n = PyList_Size(list);
         if (n < 0)
             return -1; /* Not a list */
             /* Caller should use PyErr_Occurred() if a -1 is returned. */
         for (i = 0; i < n; i++) {
             /* PyList_GetItem does not INCREF "item".
                "item" is unprotected. */
             item = PyList_GetItem(list, i); /* Can't fail */
             if (PyInt_Check(item))
                 total += PyInt_AsLong(item);
         }
         return total;
     }

PyList_GetItem()返回的item是PyObject類型,引用計數沒有被INCREF,所以函數done之後沒有對item進行DECREF。

示例2:

   long sum_sequence(PyObject *sequence)
     {
         int i, n;
         long total = 0;
         PyObject *item;
         n = PySequence_Length(sequence);
         if (n < 0)
             return -1; /* Has no length. */
             /* Caller should use PyErr_Occurred() if a -1 is returned. */
         for (i = 0; i < n; i++) {
             /* PySequence_GetItem INCREFs item. */
             item = PySequence_GetItem(sequence, i);
             if (item == NULL)
                 return -1; /* Not a sequence, or other failure */
             if (PyInt_Check(item))
                 total += PyInt_AsLong(item);
             Py_DECREF(item);
         }
         return total;
     }

與示例1不同,PySequnce_GetItem()的源碼實現中,對返回的item的引用計數進行了INCREF,所以在函數done時需要調用Py_DECREF(item)。

什麼時候不需要調用INCREF

1.對於函數中的局部變數,這些局部變數如果是PyObject對象的指針,沒有必要增加這些局部對象的引用計數。理論上,當有一個變數指向對象的時候,對象的引用計數會被+1,同時在變數離開作用域時,對象的引用計數會被-1,而這兩個操作是相互抵消的,最終對象的引用數沒有改變。使用引用計數真正的原因是防止對象在有變數指向它的時候被提前銷毀。

什麼時候需要調用INCREF

如果有任何的可能在某個對象上調用DECREF,那麼就需要保證該對象不能處於unprotected狀態。

1) 如果一個引用處於unprotected,可能會引起微妙的bug。一個常見的情況是,從list中取出元素對象,繼續操作它,但是不增加它的引用計數。PyList_GetItem 會返回一個 borrowed reference ,所以 item 處於未保護狀態。一些其他的操作可能會從 list 中將這個對象刪除(遞減它的引用計數,或者釋放它)。導致 item 成為一個懸垂指針。

bug(PyObject *list) {
         PyObject *item = PyList_GetItem(list, 0);

         PyList_SetItem(list, 1, PyInt_FromLong(0L));
         PyObject_Print(item, stdout, 0); /* BUG! */
     }

這個函數的功能:從list中取出第0個元素item(此時沒有遞增它的引用計數),然後替換list[1]為整數0,最後列印item.看起來很正常,沒有什麼問題,其實不然。

我們跟著PyList_SetItem函數的流程走一遍。list中所有元素的引用計數都是protected的,所以當把list[1]的元素替換時,必須將原來的元素的引用計數減少。假設原來的元素list[1]是一個用戶自定義的一個類,並且實現了__del__方法。如果這個類的instance的引用計數為1,當減少它的引用計數時,此instance會被釋放,會調用__del__方法。而__del__方法是python用戶自己寫的代碼,所以__del__可以是任意的python代碼,那麼是不是有可能做了某些操作導致list[0]的引用計數無效,比如在__del__方法中del list[0],假如list[0]的引用計數也是1,那麼list[0]會被釋放,而被釋放的item再次被作為參數傳遞給了PyObject_print()函數,此時會出現意想不到的行為。

解決的辦法也很多簡單:

    no_bug(PyObject *list) {
         PyObject *item = PyList_GetItem(list, 0);
         Py_INCREF(item); /* Protect item. */

         PyList_SetItem(list, 1, PyInt_FromLong(0L));
         PyObject_Print(item, stdout, 0);
         Py_DECREF(item);
     }

 This is a true story. An older version of Python contained variants of this bug and someone spent a considerable amount of time in a C debugger to figure out why his __del__() methods would fail...

2) 傳遞PyObject對象給函數,一般都是假設傳遞過來的對象的引用計數已經是protected,因此在函數內部不需要調用Py_INCREF。不過,如果想要參數存活到函數退出,可以調用Py_INCREF。

When you pass an object reference into another
    function, in general, the function borrows the reference from you
    -- if it needs to store it, it will use Py_INCREF() to become an
    independent owner.

PyDict_SetItem()就是這樣的例子,將某些東西存放在字典中,會將key和value的引用計數都加1.

 

PyTuple_SetItem()和PyList_SetItem()與PyDict_SetItem()不同,他們接管傳遞給他們的對象(偷取一個引用)。

PyTuple_SetItem的原型是PyTuple_SetItem(atuple, i, item): 如果atuple[i]當前包含了一個PyObject,則將此PyObject DECREF,然後atuple[i]設置為item。 item並不會被INCREFed

如果PyTuple_SetItem插入元素失敗,會減少item的引用計數。同樣,PyTuple_GetItem不會增加返回的item的引用計數。

PyObject *t;
PyObject *x;
x = PyInt_FromLong(1L);
PyTuple_SetItem(t, 0, x);

當x作為參數傳遞給PyTuple_SetItem函數時,那麼必須不能調用Py_DECREF,因為PyTuple_SetItem()函數實現中沒有增加x的引用計數,如果你此時人為減少x的引用計數,那麼tuple t中的元素item已經被釋放了。

當tuple t 被DECREFed,其裡面的元素都會被DECREFed。

PyTuple_SetItem這樣的設計主要是考慮到一個很常見的場景:創建一個新的對象來填充tuple或list。例如創建這樣一個tuple, (1, 2, "there")。 使用Python C API可以這樣做:

  PyObject *t;

     t = PyTuple_New(3);
     PyTuple_SetItem(t, 0, PyInt_FromLong(1L));
     PyTuple_SetItem(t, 1, PyInt_FromLong(2L));
     PyTuple_SetItem(t, 2, PyString_FromString("three"));

Note: PyTuple_SetItem是設置tuple元素的唯一的方法。 PySequence_SetItem和PyObject_SetItem都會拒絕這樣做,因為tuple是一個不可變的數據類型。

創建list與創建tuple的介面類似,PyList_New()和PyList_SetItem(),有個區別是填充list的元素可以使用PySequence_SetItem(),但是PySequence_SetItem會增加傳入的item的引用計數。

PyObject *l, *x;

 l = PyList_New(3);
 x = PyInt_FromLong(1L);
 PySequence_SetItem(l, 0, x); Py_DECREF(x);
 x = PyInt_FromLong(2L);
 PySequence_SetItem(l, 1, x); Py_DECREF(x);
 x = PyString_FromString("three");
 PySequence_SetItem(l, 2, x); Py_DECREF(x);

 

Python信奉極簡主義,上述創建tuple(list)和填充tuple(list)的代碼可以簡化為:

 PyObject *t, *l;

 t = Py_BuildValue("(iis)", 1, 2, "three");
 l = Py_BuildValue("[iis]", 1, 2, "three");

 

Two Examples:

Example 1:

 

    PyObject*
    MyFunction(void)
    {
        PyObject* temporary_list=NULL;
        PyObject* return_this=NULL;

        temporary_list = PyList_New(1);          /* Note 1 */
        if (temporary_list == NULL)
            return NULL;

        return_this = PyList_New(1);             /* Note 1 */
        if (return_this == NULL)
            Py_DECREF(temporary_list);           /* Note 2 */
            return NULL;
        }

        Py_DECREF(temporary_list);               /* Note 2 */
        return return_this;
    }

Note1: PyList_New返回的object的引用計數為1

Note2: 因為temporary_list 在函數退出時不應該存在,所以在函數返回前必須DECREFed。

 

Example 2:

    PyObject*
    MyFunction(void)
    {
        PyObject* temporary=NULL;
        PyObject* return_this=NULL;
        PyObject* tup;
        PyObject* num;
        int err;

        tup = PyTuple_New(2);
        if (tup == NULL)
            return NULL;

        err = PyTuple_SetItem(tup, 0, PyInt_FromLong(222L));  /* Note 1 */
        if (err) {
            Py_DECREF(tup);
            return NULL;
        }
        err = PyTuple_SetItem(tup, 1, PyInt_FromLong(333L));  /* Note 1 */
        if (err) {
            Py_DECREF(tup);
            return NULL;
        }

        temporary = PyTuple_Getitem(tup, 0);          /* Note 2 */
        if (temporary == NULL) {
            Py_DECREF(tup);
            return NULL;
        }

        return_this = PyTuple_Getitem(tup, 1);        /* Note 3 */
        if (return_this == NULL) {
            Py_DECREF(tup);
            /* Note 3 */
            return NULL;
        }

        /* Note 3 */
        Py_DECREF(tup);
        return return_this;
    }

Note1:如果PyTuple_SetItem失敗或者這個tuple引用計數變為0,那麼PyInt_FromLong創建的對象引用計數也被減少

Note2:PyTuple_GetItem不會增加返回的對象的引用計數

Note3:MyFunction沒有責任處理temporary的引用計數,不需要DECREF temporary


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 三天不寫代碼就手生! 把測試代碼記錄下來。 註意事項: 1. 三個方法必須一塊使用,不能只寫 System.MonitorWait(Form1, INFINITE); System.MonitorEnter(Form1); //必須 Log('Thread1 Enter'); System.Moni ...
  • 進程:qq要以一個整體的形式暴露給操作系統管理,裡面包含對各種資源的調用,記憶體的對各種資源管理的集合,就可稱之為進程。 線程:是操作系統最小的調度單位,是一串指令的集合。 進程:要操作CPU,必須要先創建一個線程,進程不能單獨執行,進程執行是調動線程,至少要有一個線程; 進程是資源的集合,線程是最小 ...
  • 在Django模板中可能會出現訪問多個URL指向同一函數,為減少代碼可以利用正則表達式實現 (?P<nid>\d+)表示此部分的名稱為nid,對應removeuserinfo(request,nid)中的nid參數 此時 模板.html 中 a標簽中的href的值可以寫為{% url "reinfo ...
  • 字元串內置方法的使用 設置字元串:st=‘hello kitty’ 1、統計元素個數 Print(st.count(‘l’)) #結果為2 2、首字母大寫 Print(st.capitalize()) #結果為Hello kitty 3、居中 Print(st.center(20,’#’)) #結果 ...
  • 重載(Overloading) (1) 方法重載是讓類以統一的方式處理不同類型數據的一種手段。 多個同名方法同時存在,具有不同的參數個數/類型。 重載Overloading是一個類中多態性的一種表現。 (2) Java的方法重載,就是在類中可以創建多個方法,它們具有相同的名字,但具有不同的參數和不同 ...
  • 我的第一篇博客,哈哈哈,記錄一下我的Python進階之路!今天寫了一個爬取天氣網歷史數據的小爬蟲。主要使用Python的requests 和BeautifulSoup模塊,核心是利用BeautifulSoup的select語句獲取需要的信息。 ...
  • 最近在公司實習期間的培訓交流中有機會接觸到SSM,然後自己花費1周的時間投入學習。談不上深刻理解其中原理,所以沒有涉及理論知識,只是淺層次的學習如何使用,在此將學習過程記錄整理出來,一方面自己備用;另一方面,分享出來和做技術的各位討論交流。 現階段的我還做不到知行合一,所以決定先學會用,然後再去深究 ...
  • 有一個獸人類 Class c=Class.forName("OrcDemo"); OrcDemo od=(OrcDemo)c.newInstance(); od.orcInfo(); apache.tomcat就是使用這種方法調用的Servlet ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...