编程思考:对象生命周期的问题
前情提要
只要寫過(guò) c/c++ 的項(xiàng)目的童鞋應(yīng)該對(duì)對(duì)象生命周期的問題記憶猶新。怕有人還不理解這個(gè)問題,筆者先介紹下什么是生命周期的問題?
一個(gè) struct 結(jié)構(gòu)體生命周期分為三個(gè)步驟:
出生:malloc 分配結(jié)構(gòu)體內(nèi)存,并且初始化;
使用:這個(gè)就是對(duì)內(nèi)存的常規(guī)使用了;
銷毀:free 釋放這個(gè)內(nèi)存塊;
最典型結(jié)構(gòu)體“生命周期”問題的場(chǎng)景就是:你在使用對(duì)象正嗨的時(shí)候,被人偷偷把對(duì)象銷毀了。舉個(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)存了,全劇終;
生命周期問題的維度
一般來(lái)講,生命周期的問題其實(shí)有兩個(gè)方面:
第一個(gè)是結(jié)構(gòu)體本身內(nèi)存的生命周期 ;
第二個(gè)是結(jié)構(gòu)體對(duì)象管理的資源( 比如資源句柄 );
?1???結(jié)構(gòu)體本身內(nèi)存
對(duì)象結(jié)構(gòu)體本身的生命周期這個(gè)很容易理解,這個(gè)就是內(nèi)存的分配和釋放。
//?步驟一:分配 obj_addr?=?malloc(...); //?步驟二:使用 ... //?步驟三:釋放 free(obj_addr);如果違反了這條(使用了已經(jīng)釋放的內(nèi)存塊),就會(huì)發(fā)生踩內(nèi)存,野指針,未定義地址等一系列奇異事件。如果沒正確釋放,那么就是內(nèi)存泄漏。
?2???對(duì)象管理的資源
這個(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 等一系列情況。
怎么才能解決生命周期的問題?
生命周期的問題是每個(gè)程序猿都可能遇到的,只要程序中涉及到資源的創(chuàng)建、使用、釋放,這三個(gè)過(guò)程,那么生命周期的問題就是你必經(jīng)之路,這是一個(gè)通用的問題。
上面我們提到生命周期問題的兩個(gè)維度,那么解決也是這兩個(gè)維度的針對(duì)性解決。遵守兩個(gè)原則:
對(duì)象在有人使用的時(shí)候不能釋放;
對(duì)象不僅要釋放自身內(nèi)存還要釋放管理的資源;
思考下:你在編程的時(shí)候,怎么處理的?
下面我從 c 這種底層語(yǔ)言,還有 Go 這種自帶 GC 的語(yǔ)言對(duì)比出發(fā),來(lái)體驗(yàn)下不同語(yǔ)言下的生命周期的問題怎么解決。
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 銷毀的時(shí)候,會(huì)從全局鏈表中摘除;
首先,list 鏈表的并發(fā)安全可以用互斥鎖來(lái)解決,但是怎么保證你取出來(lái)元素之后,還在處理的時(shí)候,一直是安全的呢(不被釋放)?
你可能會(huì)自然想到一個(gè)思路:全程在鎖內(nèi)不就可以了。
確實(shí)如此,對(duì)象的創(chuàng)建,使用,刪除,全程用鎖保護(hù),確實(shí)可以解決這個(gè)問題。但是鎖度變得非常大,在現(xiàn)實(shí)生產(chǎn)環(huán)境的編程中,很少見。
其實(shí),解決資源釋放的場(chǎng)景,有一個(gè)通用的技術(shù):引用計(jì)數(shù)。 wiki 上的解釋:
引用計(jì)數(shù)是計(jì)算機(jī)編程語(yǔ)言中的一種內(nèi)存管理技術(shù),是指將資源(可以是對(duì)象、內(nèi)存或磁盤空間等等)的被引用次數(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ì)有問題。因?yàn)樵谀闶褂闷陂g,不管別人怎么減,都不可能會(huì)到 0 。
思考下:引用計(jì)數(shù)有什么缺點(diǎn)呢?
第一個(gè)問題,非常容易出錯(cuò),加減引用一定要配對(duì),一旦有些地方多加了,或者多減了,就會(huì)引發(fā)資源問題。要么就是泄漏,要么就是使用釋放了的資源;
第二個(gè)問題,在于流程上變復(fù)雜了,因?yàn)?strong>計(jì)數(shù)為 0 的地方點(diǎn)變得不確定了。可能會(huì)出現(xiàn)在讀元素的流程上,走釋放流程;
以上兩點(diǎn),其實(shí)對(duì)程序猿的能力、細(xì)致提出了很高的要求。
Go 就厲害了
引用計(jì)數(shù)是通用的技術(shù),適用于所有的語(yǔ)言。筆者在寫 Go 的時(shí)候就用引用計(jì)數(shù)來(lái)解決過(guò)資源釋放的問題。
但后來(lái)發(fā)現(xiàn),Go 語(yǔ)言其實(shí)可以把代碼寫的更簡(jiǎn)單,Go 的創(chuàng)建則從兩個(gè)的角度解決了對(duì)象生命周期的問題:
第一,根本不讓用戶釋放內(nèi)存;
Go 的內(nèi)存,程序猿只能觸發(fā)分配,無(wú)法主動(dòng)釋放。釋放內(nèi)存的動(dòng)作完全交給了后臺(tái) GC 流程。這就很好的解決了第一個(gè)問題,由于不讓粗心的程序猿參與到資源的管理中,內(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)用,程序猿則可以把資源釋放的邏輯寫到析構(gòu)函數(shù)中,這樣就配合上了呀,就能保證:在對(duì)象永遠(yuǎn)不能被程序猿摸到的前提下,調(diào)用了析構(gòu)函數(shù),從而完成資源釋放。
?1???生命結(jié)束的回調(diào)
函數(shù)原型:
func?SetFinalizer(obj?interface{},?finalizer?interface{})參數(shù)解析:
參數(shù) obj 必須是指針類型
參數(shù) finalizer 是一個(gè)函數(shù),參數(shù)為 obj 的類型,無(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)于生命周期的問題,在 Go 里面就非常優(yōu)雅的解決了,對(duì)象內(nèi)存釋放交給了 Gc,資源釋放交給了 finalizer ,程序猿又可以躺好了。
擴(kuò)展思考
c++ 和 Python 這兩種語(yǔ)言又是怎么解決內(nèi)存的生命周期,還有資源的安全釋放呢?
提示:這兩種語(yǔ)言都有構(gòu)造函數(shù)和析構(gòu)函數(shù),但各有不同。這個(gè)問題留給讀者朋友思考。
c++ 有構(gòu)造函數(shù)和析構(gòu)函數(shù),也很方便,但是 c++ 的類卻是非常復(fù)雜的。且 c++ 是沒有 GC 的,內(nèi)存釋放的動(dòng)作還是交給了程序猿,所以在 c++ 編程中,引用計(jì)數(shù)技術(shù)還是大量使用的;
python 是一個(gè)自帶 GC ,并且提供構(gòu)造和析構(gòu)函數(shù)的。所以 python 的使用,程序猿完全不管內(nèi)存釋放,資源釋放則只需要定義在類的析構(gòu)函數(shù)里即可;
總結(jié)
生命周期的問題是老大難的問題,分為結(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ì)象生命周期的問題;
用 runtime.SetFinalizer 替代引用計(jì)數(shù)的使用,太香了;
后記
你 open ?一個(gè)文件得到句柄 fd,緊接 unlink ?這個(gè)文件,此時(shí),還可用 fd 來(lái)正常讀寫文件。直到?close? 這個(gè)文件的時(shí)候,這個(gè)文件才會(huì)永遠(yuǎn)的消失。你能猜到其中原理嗎?
~完~
往期推薦
往期推薦
自制文件系統(tǒng) —— 03 Go實(shí)戰(zhàn):hello world 的文件系統(tǒng)
假如 Go 能說(shuō)話,聽聽 GMP 的心聲
存儲(chǔ)基礎(chǔ) — 文件描述符 fd 究竟是什么?
深度剖析 Linux cp 的秘密
堅(jiān)持思考,方向比努力更重要。關(guān)注我:奇伢云存儲(chǔ)
超強(qiáng)干貨來(lái)襲 云風(fēng)專訪:近40年碼齡,通宵達(dá)旦的技術(shù)人生總結(jié)
以上是生活随笔為你收集整理的编程思考:对象生命周期的问题的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 技巧:教你一招优化 Go GC
- 下一篇: 一文搞懂一致性hash的原理和实现