天天看點

揭開defer的面紗

defer是一種延遲函數,每增加一個defer函數會将其對應的_defer結構體壓棧,在執行的時候是後壓棧的defer先執行

1、defer觸發的場景主要有三

A "defer" statement invokes a function whose execution is deferred to the moment the surrounding function returns, either because the surrounding function executed a return statement, reached the end of its function body, or because the corresponding goroutine is panicking

1)主函數return傳回時,在return執行之前

2)主函數執行結束時

3)目前goroutine發生panic

2、官方對defer也提出3條規則

1)延遲函數的參數在defer語句出現時就已經确定下來了

這是因為在defer語句出現的時候,同時把需要的參數拷貝到棧上,後續主函數對該參數進行改變,不會影響defer中的值

但是指針的情況下,我們可以看下有什麼不同:

2)延遲函數執行按後進先出順序執行,即先出現的defer最後執行

3)延遲函數可以操作主函數的具名傳回值

可以先看一下_defer結構體字段,無特殊版本标記都是1.2版本出現的字段

通過看彙編代碼,可以看到在1.14版本,會有如下三種情況處理defer

在1.12版本使用的是堆配置設定,1.13版本加入棧配置設定,現在1.14版本又加入開放源碼,可以看到堆配置設定是最後的兜底方案。現在我們會來介紹每一種方案。

聲明defer關鍵字處使用deferproc() 注冊defer處理函數,将對應的_defer結構體值拷貝到堆上。

對于新建立好的defer結構體會添加在defer連結清單的表頭,goroutine的defer指針指向連結清單的表頭,這也是為什麼倒序執行defer的原因。

揭開defer的面紗

沒有捕獲清單(沒有外部函數使用的變量)的funcval(閉包) ,在編譯階段會進行優化 在隻讀資料段配置設定一個共用的funcval結構體。但當有捕獲清單的funcval 在堆裡配置設定funcval結構體的大小,捕獲清單使用的變量在堆中會存儲該變量,其他地方使用該變量則使用該變量的位址,是以值改變的時候引用的地方同時改變

主要流程是擷取一個siz大小的_defer結構體,可以在協程對應的處理器的defer池擷取,若擷取不到,需要在堆上配置設定size大小的結構體,然後将該結構體放在連結清單頭。

在ret指令傳回時,插入deferreturn(),将defer連結清單的頭取出執行。  執行時,會将值從堆上拷貝到棧上。

分析go1.12 defer的缺點

_defer結構體是堆配置設定,即使有預配置設定的defer池,也需要在堆上擷取和釋放,參數還要在堆棧間來回拷貝

使用連結清單注冊defer資訊,連結清單本身操作較慢

聲明defer關鍵字處使用deferproc() 注冊defer處理函數,把棧上的defer結構體注冊到defer連結清單中。

defer執行時,依然是通過deferreturn實作的,在defer函數執行時拷貝參數,不過不是在堆棧間,而是在棧上的局部變量空間拷貝到棧上的參數空間,性能提升30%

是以1.3的優化點:主要減少defer資訊的堆配置設定。但是在顯示和隐式的defer處理(例如for循環)還是使用defer1.2

1)開放編碼的使用是有條件的,滿足以下3個條件才會啟用開放源碼

延遲比特和延遲記錄是開放源碼的主要結構:

延遲比特中的每一個比特位都表示該位對應的 defer 關鍵字是否需要被執行。例如下面最後1位是1,說明接下來該位的defer會執行,使用或運算,把df的第一位置為1,标志該位可以執行,在執行的時候判斷即可,判斷完置為0,避免重複執行

揭開defer的面紗

在編譯階段插入代碼,把defer的執行邏輯展開在所屬函數内,進而免于建立defer結構體,去了構造defer連結清單項,并注冊到連結清單的過程,不需要建立defer連結清單。

openDeferRecord主要記錄了defer的資訊儲存在openDeferInfo結構體中。

2)在延遲執行的時候,調用deferreturn函數進行執行。把defer需要的參數定義為局部變量,在函數傳回前,直接調用defer的函數

在執行的時候先擷取deferBits,然後判斷哪個defer該進行執行

1.14的缺點是:在某個defer函數執行過程中發生panic,後面的defer就不會被執行,就要去執行deferl連結清單了,但是這時open coded defer并沒有注冊到連結清單,這個時候需要額外通過棧掃描的方式來實作,是以panic變得慢了,但是panic發生的幾率更小

這就是defer三種實作的原理~