ホームページ >バックエンド開発 >Golang >golang defer の実装原理を詳しく説明した記事

golang defer の実装原理を詳しく説明した記事

藏色散人
藏色散人転載
2021-09-09 15:22:582686ブラウズ

この記事は、go language チュートリアル コラムで golang defer の実装原理を紹介するもので、困っている友人の役に立てば幸いです。

defer は golang によって提供されるキーワードで、関数またはメソッドが実行を完了して戻った後に呼び出されます。
各 defer は defer 関数をスタックにプッシュします。関数またはメソッドが呼び出されると、実行のためにスタックから取り出されます。したがって、複数の defer の実行順序は最初から最後です。

for i := 0; i <= 3; i++ {
    defer fmt.Print(i)
}
//输出结果时 3,2,1,0

defer トリガーのタイミング

公式 Web サイトでは、次のように明確に説明されています。
「defer」ステートメントは、実行が周囲のタイミングまで延期される関数を呼び出します。周囲の関数が return ステートメントを実行したか、その関数本体の最後に到達したか、または対応するゴルーチンがパニックしているため、関数は戻ります。

  1. defer ステートメントでラップされた関数が戻るとき
  2. defer ステートメントでラップされた関数が最後まで実行されたとき
  3. 現在の goroutine がパニックになったとき

        //输出结果:return前执行defer
       func f1() {
           defer fmt.Println("return前执行defer")
           return 
       }
    
       //输出结果:函数执行
       // 函数执行到最后
       func f2() {
           defer fmt.Println("函数执行到最后")
           fmt.Println("函数执行")
       }
    
       //输出结果:panic前  第一个defer在Panic发生时执行,第二个defer在Panic之后声明,不能执行到
       func f3() {
           defer fmt.Println("panic前")
           panic("panic中")
           defer fmt.Println("panic后")
       }

defer, return,戻り値の実行順序

まずは 3 つの例を見てみましょう

func f1() int { //匿名返回值
        var r int = 6
        defer func() {
                r *= 7
        }()
        return r
}

func f2() (r int) { //有名返回值
        defer func() {
                r *= 7
        }()
        return 6
}

func f3() (r int) { //有名返回值
    defer func(r int) {
        r *= 7
    }(r)
    return 6
}

f1 の実行結果は 6、f2 の実行結果は 42、f3 の実行結果は 6
golang の公式ドキュメントでは、return、defer、戻り値の実行順序が紹介されています。
周囲の関数が明示的な return ステートメントを通じて返される場合、遅延関数は、その return ステートメントによって結果パラメーターが設定された後に実行されます。ただし、関数が呼び出し元に戻る前に .

1. 最初に戻り値を割り当てます
2. defer ステートメントを実行します
3 . 関数 return を return

f1 の結果は 6 になります。 f1 は匿名戻り値です。匿名戻り値は return 実行時に宣言されます。したがって、defer が宣言されている場合、匿名戻り値にアクセスできません。defer を変更しても戻り値には影響しません。
f2 は最初に戻り値 r (r=6) を割り当て、defer ステートメントを実行し、defer が r (r = 42) を変更してから、関数が戻ります。
f3 は名前付き戻り値ですが、 r が defer のパラメータとして使用されているため、 defer を宣言するとパラメータがコピーされて渡されるため、 defer は defer 関数のローカル パラメータにのみ影響し、呼び出しには影響しません。関数の戻り値。

クロージャと無名関数
無名関数: 関数名のない関数。
クロージャ: 別の関数のスコープ内で変数を使用できる関数。

for i := 0; i <= 3; i++ {
    defer func() {
        fmt.Print(i)
    }
}
//输出结果时 3,3,3,3
因为defer函数的i是对for循环i的引用,defer延迟执行,for循环到最后i是3,到defer执行时i就 
是3

for i := 0; i <= 3; i++ {
    defer func(i int) {
        fmt.Print(i)
    }(i)
}
//输出结果时 3,2,1,0
因为defer函数的i是在defer声明的时候,就当作defer参数传递到defer函数中

defer ソース コードの分析
defer の実装ソース コードは runtime.deferproc にあります
次に、関数が戻る前に runtime.deferreturn 関数を実行します。
まず遅延構造を理解します。

    type _defer struct {
            siz     int32 
            started bool
            sp      uintptr // sp at time of defer
            pc      uintptr
            fn      *funcval
            _panic  *_panic // panic that is running defer
            link    *_defer
    }

sp と pc はそれぞれスタック ポインターと呼び出し元のプログラム カウンターを指します。fn は defer キーワードに渡される関数、Panic は遅延を引き起こすパニックです。実行されます。
defer キーワードが検出されるたびに、defer 関数は runtime.deferproc に変換されます。
deferproc は newdefer を通じて遅延関数を作成し、この新しい遅延関数を現在の goroutine の _defer リンク リストにハングします

    func deferproc(siz int32, fn *funcval) { // arguments of fn follow fn
            sp := getcallersp()
            argp := uintptr(unsafe.Pointer(&fn)) + unsafe.Sizeof(fn)
            callerpc := getcallerpc()

            d := newdefer(siz)
            if d._panic != nil {
                    throw("deferproc: d.panic != nil after newdefer")
            }
            d.fn = fn
            d.pc = callerpc
            d.sp = sp
            switch siz {
            case 0:
                    // Do nothing.
            case sys.PtrSize:
                    *(*uintptr)(deferArgs(d)) = *(*uintptr)(unsafe.Pointer(argp))
            default:
                    memmove(deferArgs(d), unsafe.Pointer(argp), uintptr(siz))
            }
            return0()
    }

newdefer は、まず sched と current p の deferpool から _defer 構造体を取り出します。deferpool に _defer がない場合は、新しい _defer が初期化されます。
_defer は現在の g に関連付けられているため、defer は現在の g に対してのみ有効です。
d.link = gp._defer
gp._defer = d //リンク リストを使用して、現在の g のすべての defer を接続します。

    func newdefer(siz int32) *_defer {
            var d *_defer
            sc := deferclass(uintptr(siz))
            gp := getg()
            if sc < uintptr(len(p{}.deferpool)) {
                    pp := gp.m.p.ptr()
                    if len(pp.deferpool[sc]) == 0 && sched.deferpool[sc] != nil { 
                            .....
                            d := sched.deferpool[sc]
                            sched.deferpool[sc] = d.link
                            d.link = nil
                            pp.deferpool[sc] = append(pp.deferpool[sc], d)
                    }
                    if n := len(pp.deferpool[sc]); n > 0 {
                            d = pp.deferpool[sc][n-1]
                            pp.deferpool[sc][n-1] = nil
                            pp.deferpool[sc] = pp.deferpool[sc][:n-1]
                    }
            }
            ......
            d.siz = siz
            d.link = gp._defer
            gp._defer = d
            return d
    }

deferreturn 現在の g から _defer リンク リストを取り出し、それを実行すると、_defer を呼び出すたびに、freedefer が _defer 構造体を解放し、_defer 構造体を現在の p の deferpool に置きます。

defer パフォーマンス分析
defer は、開発においてリソースの解放やパニックのキャプチャなどに非常に役立ちます。一部の開発者は、遅延がプログラムのパフォーマンスに及ぼす影響を考慮しておらず、プログラム内で遅延を悪用している可能性があります。
パフォーマンス テストでは、遅延が依然としてパフォーマンスにある程度の影響を与えていることがわかります。 Yuchen の Go パフォーマンス最適化のヒント 4/1 には、defer ステートメントによって生じる追加のオーバーヘッドに関するテストがいくつかあります。

テスト コード

    var mu sync.Mutex
    func noDeferLock() {
        mu.Lock()
        mu.Unlock()
    }   

    func deferLock() {
        mu.Lock()
        defer mu.Unlock()
    }          
    
    func BenchmarkNoDefer(b *testing.B) {
        for i := 0; i < b.N; i++ {
            noDeferLock()
        }
    }
    
    func BenchmarkDefer(b *testing.B) {
        for i := 0; i < b.N; i++ {
            deferLock()
    }

テスト結果:

    BenchmarkNoDefer-4      100000000               11.1 ns/op
    BenchmarkDefer-4        36367237                33.1 ns/op

前回のソース コード分析から、遅延が発生することがわかります。最初に deferproc を呼び出すと、パラメーターがコピーされ、 deferreturn も関連情報を抽出して実行が遅延します。これらはステートメントを直接呼び出すよりもコストがかかります。

遅延のパフォーマンスは高くありません。各遅延には 20ns かかります。関数内で複数回発生すると、パフォーマンスの消費は 20ns*n になります。CPU リソースの累積的な浪費は非常に大きくなります。

解決策: 例外キャプチャが必要な場合を除き、defer を使用する必要がありますが、その他のリソースリサイクルの延期については、失敗を判断した後に goto を使用してリソースリサイクルのコード領域にジャンプできます。競合リソースについては、使用後すぐにリソースを解放できるため、競合リソースを最適に使用できます。

golang 関連の知識の詳細については、golangチュートリアル コラムをご覧ください!

以上がgolang defer の実装原理を詳しく説明した記事の詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。

声明:
この記事はsegmentfault.comで複製されています。侵害がある場合は、admin@php.cn までご連絡ください。