34

Go のsyncパッケージにはMutex. 残念ながら、再帰的ではありません。Goで再帰ロックを実装する最良の方法は何ですか?

4

3 に答える 3

41

あなたの質問に直接答えなくてすみません:

私見、Goで再帰ロックを実装する最良の方法は、それらを実装しないことですが、そもそもそれらを必要としないようにコードを再設計することです。それらに対する欲求は、何らかの(ここでは不明な)問題に対する間違ったアプローチが使用されていることを示している可能性が高いと思います。

上記の主張の間接的な「証拠」として: 再帰的ロックは、ミューテックスを含む/いくつかの通常の状況に対する一般的な/正しいアプローチである場合、遅かれ早かれ標準ライブラリに含まれます。

最後に、重要なことを言い忘れましたが、Go 開発チームの Russ Cox がここに書いたことhttps://groups.google.com/d/msg/golang-nuts/XqW1qcuZgKg/Ui3nQkeLV80J :

再帰的 (別名再入可能) なミューテックスは悪い考えです。ミューテックスを使用する基本的な理由は、ミューテックスが不変条件を保護することです。おそらく、「p.Prev.Next == p for all elements for the ring」のような内部不変条件、または「my local variable x is equal to p.Prev」のような外部不変条件です。 ."

ミューテックスをロックすると、「保持する不変条件が必要」であり、おそらく「それらの不変条件を一時的に破る」と断言されます。ミューテックスを解放すると、「私はもはやそれらの不変条件に依存していません」および「それらを壊した場合、私はそれらを復元しました」と主張します。

ミューテックスが不変条件を保護することを理解することは、ミューテックスが必要な場所と必要でない場所を特定するために不可欠です。たとえば、アトミックなインクリメントおよびデクリメント命令で更新された共有カウンターにはミューテックスが必要ですか? それは不変条件に依存します。唯一の不変条件が、i がインクリメントされ、d がデクリメントされた後のカウンターの値が i - d である場合、命令の原子性によって不変条件が保証されます。ミューテックスは必要ありません。しかし、カウンタが他のデータ構造と同期している必要がある場合 (リストの要素数をカウントする場合など)、個々の操作の原子性では不十分です。他の何か (多くの場合ミューテックス) は、上位レベルの不変条件を保護する必要があります。これが、Go でのマップ操作がアトミックであることが保証されていない理由です。典型的なケースでは、費用がかかるだけでメリットはありません。

再帰的ミューテックスを見てみましょう。次のようなコードがあるとします。

     func F() {
             mu.Lock()
             ... do some stuff ...
             G()
             ... do some more stuff ...
             mu.Unlock()
     }

     func G() {
             mu.Lock()
             ... do some stuff ...
             mu.Unlock()
     }

通常、mu.Lock への呼び出しが返されると、呼び出し元のコードは、保護された不変条件が保持されていると想定できるようになり、mu.Unlock を呼び出すまで続きます。

再帰的ミューテックスの実装では、G の mu.Lock および mu.Unlock 呼び出しが、F または現在のスレッドが既に mu を保持しているその他のコンテキスト内から呼び出された場合にノーオペレーションになります。mu がそのような実装を使用した場合、mu.Lock が G 内に戻ると、不変条件が保持される場合と保持されない場合があります。それは、G を呼び出す前に F が何をしたかに依存します。おそらく F は、G がそれらの不変式を必要としており、それらを破っていることにさえ気づいていませんでした (特に複雑なコードでは完全に可能です)。

再帰的ミューテックスは不変条件を保護しません。ミューテックスには 1 つのジョブしかなく、再帰的ミューテックスはそれを行いません。

あなたが書いたかのように、それらにはもっと単純な問題があります

     func F() {
             mu.Lock()
             ... do some stuff
     }

シングルスレッドのテストではバグを見つけることはできません。しかし、これはより大きな問題の特殊なケースにすぎません。つまり、ミューテックスが保護することを意図した不変条件について、まったく保証を提供しないということです。

ミューテックスを保持するかどうかに関係なく呼び出すことができる機能を実装する必要がある場合、最も明確なことは、2 つのバージョンを作成することです。たとえば、上記の G の代わりに、次のように書くことができます。

     // To be called with mu already held.
     // Caller must be careful to ensure that ...
     func g() {
             ... do some stuff ...
     }

     func G() {
             mu.Lock()
             g()
             mu.Unlock()
     }

または、両方ともエクスポートされていない場合は、g と gLocked です。

最終的には TryLock が必要になると確信しています。そのためのCLを送ってください。タイムアウト付きのロックはそれほど重要ではないようですが、クリーンな実装 (私は知りません) があれば、おそらく大丈夫でしょう。再帰的ミューテックスを実装する CL を送信しないでください。

再帰的ミューテックスは単なる間違いであり、バグにとって居心地の良い家にすぎません。

ラス

于 2013-02-03T10:21:28.403 に答える
3

sync.Mutexsync.Condから再帰的なロックを非常に簡単に作成できます。いくつかのアイデアについては、こちらの付録 Aを参照してください。

Go ランタイムがゴルーチン ID の概念を公開しないという事実を除いて。これは、人々が goroutine ローカル ストレージでばかげたことをするのを止めるためであり、おそらく設計者が goroutine Id が必要な場合、それは間違っていると考えていることを示しています。

もちろん、本当に必要な場合は、少し C を使用してランタイムから goroutine Id を掘り出すことができます。そのスレッドを読んで、Go の設計者がなぜそれを悪い考えだと考えているのかを確認してください。

于 2013-02-03T14:28:38.077 に答える
-10

すでに確立されているように、これは並行性の観点からは悲惨で恐ろしい、ひどい、そしてひどい考えです。

とにかく、あなたの質問は本当にGoの型システムに関するものなので、再帰メソッドを使用して型を定義する方法は次のとおりです。

type Foo struct{}

func (f Foo) Bar() { fmt.Println("bar") }

type FooChain struct {
    Foo
    child *FooChain
}

func (f FooChain) Bar() {
    if f.child != nil {
        f.child.Bar()
    }
    f.Foo.Bar()
}

func main() {
    fmt.Println("no children")
    f := new(FooChain)
    f.Bar()

    for i := 0; i < 10; i++ {
        f = &FooChain{Foo{}, f}
    }
    fmt.Println("with children")
    f.Bar()
}

http://play.golang.org/p/mPBHKpgxnd

于 2013-02-03T17:08:41.077 に答える