
前情提要

只要寫(xiě)過(guò) c/c++ 的項(xiàng)目的童鞋應(yīng)該對(duì)對(duì)象生命周期的問(wèn)題記憶猶新。怕有人還不理解這個(gè)問(wèn)題,筆者先介紹下什么是生命周期的問(wèn)題?
一個(gè) struct 結(jié)構(gòu)體生命周期分為三個(gè)步驟:
出生: malloc
分配結(jié)構(gòu)體內(nèi)存,并且初始化;使用:這個(gè)就是對(duì)內(nèi)存的常規(guī)使用了; 銷(xiāo)毀: free
釋放這個(gè)內(nèi)存塊;

最典型結(jié)構(gòu)體“生命周期”問(wèn)題的場(chǎng)景就是:你在使用對(duì)象正嗨的時(shí)候,被人偷偷把對(duì)象銷(xiāo)毀了。舉個(gè)例子:
12:00 時(shí)刻:ObjectA 內(nèi)存 malloc 出來(lái),地址為 0x12345 ; 12:10 時(shí)刻:ObjectA 內(nèi)存地址 0x12345 釋放了; 12:12 時(shí)刻:程序猿小明拿到了 ObjectA 的地址 0x12345 ,準(zhǔn)備大干一場(chǎng)(但他并不知道的是,這個(gè) ObjectA 結(jié)構(gòu)體已經(jīng)結(jié)束了生命,0x12345 地址已經(jīng)被釋放了)于是,踩內(nèi)存了,全劇終;

生命周期問(wèn)題的維度

一般來(lái)講,生命周期的問(wèn)題其實(shí)有兩個(gè)方面:
第一個(gè)是結(jié)構(gòu)體本身內(nèi)存的生命周期 ; 第二個(gè)是結(jié)構(gòu)體對(duì)象管理的資源( 比如資源句柄 );
對(duì)象結(jié)構(gòu)體本身的生命周期這個(gè)很容易理解,這個(gè)就是內(nèi)存的分配和釋放。
// 步驟一:分配
obj_addr = malloc(...);
// 步驟二:使用 ...
// 步驟三:釋放
free(obj_addr);
如果違反了這條(使用了已經(jīng)釋放的內(nèi)存塊),就會(huì)發(fā)生踩內(nèi)存,野指針,未定義地址等一系列奇異事件。如果沒(méi)正確釋放,那么就是內(nèi)存泄漏。
這個(gè)也很容易理解,比如一個(gè)代表 fd_t
的結(jié)構(gòu)體,里面有一個(gè)整型字段,代表這個(gè)結(jié)構(gòu)體管理的一個(gè)文件句柄。當(dāng) fd_t
結(jié)構(gòu)體內(nèi)存被釋放的時(shí)候,它管理的文件句柄 sys_fd
也是需要 close
的。
struct fd_t {
int sys_fd; // 系統(tǒng)句柄(這個(gè)需要在合適的時(shí)機(jī)釋放)
struct list_head list; // 鏈表掛接件
//...
};
如果違反了這條(使用釋放了的資源,比如句柄),那么就會(huì)出現(xiàn) bad descriptor
等一系列情況。

怎么才能解決生命周期的問(wèn)題?

生命周期的問(wèn)題是每個(gè)程序猿都可能遇到的,只要程序中涉及到資源的創(chuàng)建、使用、釋放,這三個(gè)過(guò)程,那么生命周期的問(wèn)題就是你必經(jīng)之路,這是一個(gè)通用的問(wèn)題。
上面我們提到生命周期問(wèn)題的兩個(gè)維度,那么解決也是這兩個(gè)維度的針對(duì)性解決。遵守兩個(gè)原則:
對(duì)象在有人使用的時(shí)候不能釋放; 對(duì)象不僅要釋放自身內(nèi)存還要釋放管理的資源;
思考下:你在編程的時(shí)候,怎么處理的?
下面我從 c 這種底層語(yǔ)言,還有 Go 這種自帶 GC 的語(yǔ)言對(duì)比出發(fā),來(lái)體驗(yàn)下不同語(yǔ)言下的生命周期的問(wèn)題怎么解決。

c 編程的慣例

c 怎么才能保證內(nèi)存的安全,資源的安全釋放呢?
以下面的場(chǎng)景舉例:
現(xiàn)在有一個(gè) fd_t 的 list 鏈表,為了保護(hù)這個(gè)鏈表,用一個(gè)互斥鎖來(lái)保護(hù) ; 創(chuàng)建 fd_t 的時(shí)候,需要添加進(jìn) list(添加會(huì)加互斥鎖); 正常使用的時(shí)候,會(huì)遍歷 list ,取合適的元素使用; fd_t 銷(xiāo)毀的時(shí)候,會(huì)從全局鏈表中摘除;
首先,list 鏈表的并發(fā)安全可以用互斥鎖來(lái)解決,但是怎么保證你取出來(lái)元素之后,還在處理的時(shí)候,一直是安全的呢(不被釋放)?
你可能會(huì)自然想到一個(gè)思路:全程在鎖內(nèi)不就可以了。
確實(shí)如此,對(duì)象的創(chuàng)建,使用,刪除,全程用鎖保護(hù),確實(shí)可以解決這個(gè)問(wèn)題。但是鎖度變得非常大,在現(xiàn)實(shí)生產(chǎn)環(huán)境的編程中,很少見(jiàn)。
其實(shí),解決資源釋放的場(chǎng)景,有一個(gè)通用的技術(shù):引用計(jì)數(shù)。 wiki 上的解釋?zhuān)?/p>
引用計(jì)數(shù)是計(jì)算機(jī)編程語(yǔ)言中的一種內(nèi)存管理技術(shù),是指將資源(可以是對(duì)象、內(nèi)存或磁盤(pán)空間等等)的被引用次數(shù)保存起來(lái),當(dāng)被引用次數(shù)變?yōu)榱銜r(shí)就將其釋放的過(guò)程。使用引用計(jì)數(shù)技術(shù)可以實(shí)現(xiàn)自動(dòng)資源管理的目的。
引用計(jì)數(shù)是一種通用的資源管理技術(shù),簡(jiǎn)述引用計(jì)數(shù)用法:
資源初始化的時(shí)候,計(jì)數(shù)為 1 ; 就是在資源獲取的時(shí)候,對(duì)資源計(jì)數(shù)加 1 ; 資源使用完成的時(shí)候,對(duì)資源計(jì)數(shù)減 1 ; 計(jì)數(shù)為 0 的時(shí)候,走釋放流程 ;
這樣,只需要用戶對(duì)資源的使用上遵守一個(gè)規(guī)則:獲取的時(shí)候,計(jì)數(shù)加 1,處理完了,計(jì)數(shù)減 1 ,就能保證不會(huì)有問(wèn)題。因?yàn)樵谀闶褂闷陂g,不管別人怎么減,都不可能會(huì)到 0 。
思考下:引用計(jì)數(shù)有什么缺點(diǎn)呢?
第一個(gè)問(wèn)題,非常容易出錯(cuò),加減引用一定要配對(duì),一旦有些地方多加了,或者多減了,就會(huì)引發(fā)資源問(wèn)題。要么就是泄漏,要么就是使用釋放了的資源; 第二個(gè)問(wèn)題,在于流程上變復(fù)雜了,因?yàn)?strong>計(jì)數(shù)為 0 的地方點(diǎn)變得不確定了。可能會(huì)出現(xiàn)在讀元素的流程上,走釋放流程;
以上兩點(diǎn),其實(shí)對(duì)程序猿的能力、細(xì)致提出了很高的要求。

Go 就厲害了

引用計(jì)數(shù)是通用的技術(shù),適用于所有的語(yǔ)言。筆者在寫(xiě) Go 的時(shí)候就用引用計(jì)數(shù)來(lái)解決過(guò)資源釋放的問(wèn)題。
但后來(lái)發(fā)現(xiàn),Go 語(yǔ)言其實(shí)可以把代碼寫(xiě)的更簡(jiǎn)單,Go 的創(chuàng)建則從兩個(gè)的角度解決了對(duì)象生命周期的問(wèn)題:
第一,根本不讓用戶釋放內(nèi)存;
Go 的內(nèi)存,程序猿只能觸發(fā)分配,無(wú)法主動(dòng)釋放。釋放內(nèi)存的動(dòng)作完全交給了后臺(tái) GC 流程。這就很好的解決了第一個(gè)問(wèn)題,由于不讓粗心的程序猿參與到資源的管理中,內(nèi)存資源的管理完全由框架管理(框架強(qiáng),則我強(qiáng),嘿嘿),根本就不用擔(dān)心會(huì)被程序猿用到生命終結(jié)的內(nèi)存塊。
第二,提供析構(gòu)回調(diào)函數(shù)機(jī)制;
上面說(shuō)了,GC 能夠保證內(nèi)存結(jié)構(gòu)體本身的安全性,但是一些句柄資源的釋放卻無(wú)法通過(guò)上面保證,怎么辦?
Go 提供了一個(gè)非常好的辦法:設(shè)置析構(gòu)函數(shù)。使用 runtime.SetFinalizer
來(lái)設(shè)置,將一個(gè)對(duì)象的地址和一個(gè)析構(gòu)函數(shù)綁定起來(lái),并且注冊(cè)到框架里。當(dāng)對(duì)象被 GC 的時(shí)候,析構(gòu)函數(shù)將會(huì)被框架調(diào)用,程序猿則可以把資源釋放的邏輯寫(xiě)到析構(gòu)函數(shù)中,這樣就配合上了呀,就能保證:在對(duì)象永遠(yuǎn)不能被程序猿摸到的前提下,調(diào)用了析構(gòu)函數(shù),從而完成資源釋放。
函數(shù)原型:
func SetFinalizer(obj interface{}, finalizer interface{})
參數(shù)解析:
參數(shù) obj
必須是指針類(lèi)型參數(shù) finalizer
是一個(gè)函數(shù),參數(shù)為 obj 的類(lèi)型,無(wú)返回值
函數(shù)調(diào)用 runtime.SetFinalizer
把 obj
和 finalizer
關(guān)聯(lián)起來(lái)。對(duì)象 obj 被 Gc 的時(shí)候,Go 會(huì)自動(dòng)調(diào)用 finalizer
函數(shù),并且 obj 作為參數(shù)傳入。
就這樣,關(guān)于生命周期的問(wèn)題,在 Go 里面就非常優(yōu)雅的解決了,對(duì)象內(nèi)存釋放交給了 Gc,資源釋放交給了 finalizer
,程序猿又可以躺好了。

擴(kuò)展思考

c++ 和 Python 這兩種語(yǔ)言又是怎么解決內(nèi)存的生命周期,還有資源的安全釋放呢?
提示:這兩種語(yǔ)言都有構(gòu)造函數(shù)和析構(gòu)函數(shù),但各有不同。這個(gè)問(wèn)題留給讀者朋友思考。
c++ 有構(gòu)造函數(shù)和析構(gòu)函數(shù),也很方便,但是 c++ 的類(lèi)卻是非常復(fù)雜的。且 c++ 是沒(méi)有 GC 的,內(nèi)存釋放的動(dòng)作還是交給了程序猿,所以在 c++ 編程中,引用計(jì)數(shù)技術(shù)還是大量使用的; python 是一個(gè)自帶 GC ,并且提供構(gòu)造和析構(gòu)函數(shù)的。所以 python 的使用,程序猿完全不管內(nèi)存釋放,資源釋放則只需要定義在類(lèi)的析構(gòu)函數(shù)里即可;

總結(jié)

生命周期的問(wèn)題是老大難的問(wèn)題,分為結(jié)構(gòu)內(nèi)存的安全釋放,內(nèi)部管理資源的安全釋放兩個(gè)維度; c/c++ 大量采用引用計(jì)數(shù)技術(shù)來(lái)完成對(duì)資源的安全釋放; 引用計(jì)數(shù)的難點(diǎn)在于加減計(jì)數(shù)的配套使用,并且釋放的現(xiàn)場(chǎng)不確定; Go 通過(guò)內(nèi)存自動(dòng) Gc ,且提供析構(gòu)函數(shù)綁定到對(duì)象地址的方法,從而完美解決了對(duì)象生命周期的問(wèn)題; 用 runtime.SetFinalizer
替代引用計(jì)數(shù)的使用,太香了;

后記

你 open 一個(gè)文件得到句柄 fd,緊接 unlink 這個(gè)文件,此時(shí),還可用 fd 來(lái)正常讀寫(xiě)文件。直到 close 這個(gè)文件的時(shí)候,這個(gè)文件才會(huì)永遠(yuǎn)的消失。你能猜到其中原理嗎?
~完~

往期推薦

往期推薦
堅(jiān)持思考,方向比努力更重要。關(guān)注我:奇伢云存儲(chǔ)