Подтвердить что ты не робот

Рекурсивная блокировка в Go

Пакет

Go sync имеет Mutex. К сожалению, это не рекурсивно. Какой лучший способ реализовать рекурсивные блокировки в Go?

4b9b3361

Ответ 1

Извините, что не ответил на ваш вопрос напрямую:

IMHO, лучший способ реализации рекурсивных блокировок в Go - не реализовать их, а скорее перепроектировать ваш код, чтобы они не нуждались в них в первую очередь. Вероятно, я думаю, что стремление к ним указывает на неправильный подход к какой-то (неизвестной здесь) проблеме.

Как косвенное "доказательство" вышеуказанного утверждения: будет ли рекурсивная блокировка обычным/правильным подходом к/некоторым обычным ситуациям, связанным с мьютексами, он рано или поздно включается в стандартную библиотеку.

И, наконец, последнее, но не менее важное: что Russ Cox из команды разработчиков Go написал здесь https://groups.google.com/d/msg/golang-nuts/XqW1qcuZgKg/Ui3nQkeLV80J:

Рекурсивные (так называемые реентерабельные) мьютексы - плохая идея. Основной причиной использования мьютекса является то, что мьютексы защищать инварианты, возможно, внутренние инварианты типа "p.Prev.Next == p для всех элементов кольца", или, возможно, внешние инварианты типа "моя локальная переменная x равна p.Prev."

Блокировка мьютекса утверждает: "Мне нужны инварианты для хранения" и, возможно, "я временно нарушу эти инварианты". Освобождение мьютекса утверждает: "Я больше не зависим от тех инвариантов" и "Если я сломаю их, я их восстановил".

Понимание того, что мьютексы защищают инварианты, имеет важное значение для определяя, где мьютексы необходимы, а где нет. Например, общий счетчик, обновленный с помощью атомного Инструкции по приращению и декременту нуждаются в мьютексе? Это зависит от инвариантов. Если единственным инвариантом является то, что счетчик имеет значение я - d после я приращений и 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 вызовы будут no-ops при вызове из F или любой другой контекст, в котором текущий поток уже содержит mu. Если mu использовала такую ​​реализацию, тогда, когда mu.Lock возвращается внутри G, инварианты могут или не могут выполняться. Это зависит на том, что сделал F до вызова G. Возможно, F даже не осознавал что G нуждался в этих инвариантах и ​​сломал их (полностью возможно, особенно в сложном коде).

Рекурсивные мьютексы не защищают инварианты. Мьютексы имеют только одно задание, а рекурсивные мьютексы этого не делают.

С ними возникают более простые проблемы, например, если вы написали

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

вы никогда не найдете ошибку в однопоточном тестировании. Но это всего лишь частный случай большой проблемы, что они не предоставляют никаких гарантий вообще о инварианты, которые мьютекс призван защищать.

Если вам нужно реализовать функциональность, которая может быть вызвана с мьютексом или без него, самая ясная вещь состоит в том, чтобы написать две версии. Например, вместо приведенного выше 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, чтобы реализует рекурсивные мьютексы.

Рекурсивные мьютексы - это просто ошибка, не более удобный дом для ошибок.

Расс

Ответ 2

Вы могли бы легко сделать рекурсивную блокировку sync.Mutex и sync.Cond. См. Приложение A здесь для некоторых идей.

За исключением за то, что во время выполнения Go не отображается какое-либо понятие идентификатора goroutine. Это означает, что люди не делают глупых вещей с локальным хранилищем goroutine и, вероятно, указывают на то, что дизайнеры считают, что если вам нужен идентификатор goroutine, вы делаете это неправильно.

Конечно, вы можете выкопать идентификатор goroutine из среды выполнения с бит C, если хотите. Возможно, вы захотите прочитать этот поток, чтобы понять, почему дизайнеры Go считают, что это плохая идея.

Ответ 3

как уже было установлено, это ужасная, ужасная, ужасная и страшная идея с точки зрения concurrency.

В любом случае, так как ваш вопрос действительно о системе типа 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