ProbemosLock in Go (context.Context)

¡Hola!



En este artículo, me gustaría decirle cómo podría crear su propio RWMutex, pero con la capacidad de omitir el bloqueo por tiempo de espera o activando el contexto. Es decir, implemente TryLock (context.Context) y RTryLock (context.Context), pero para su propio Mutex.



imagen



La imagen muestra cómo verter agua en un cuello muy estrecho.



Para empezar, conviene aclarar que para el 99% de las tareas dichos métodos no son necesarios en absoluto. Se vuelven necesarios cuando el recurso bloqueado puede que no se libere durante mucho tiempo. Me gustaría señalar que si un recurso bloqueado permanece ocupado durante mucho tiempo, vale la pena intentar optimizar la lógica al principio de tal manera que se minimice el tiempo de bloqueo.



Para obtener más información, consulte Dancing with Mutexes in Go en el ejemplo 2.



Pero si, sin embargo, tenemos que tener una retención prolongada de un flujo de recursos, entonces me parece que será difícil prescindir de TryLock.



, , atomic, . , . , , . , , .



Mutex:



// RWTMutex - Read Write and Try Mutex
type RWTMutex struct {
    state int32
    mx    sync.Mutex
    ch    chan struct{}
}


state — mutex, atomic.AddInt32, atomic.LoadInt32 atomic.CompareAndSwapInt32



ch — , .



mx — , , .



:



// TryLock - try locks mutex with context
func (m *RWTMutex) TryLock(ctx context.Context) bool {
    if atomic.CompareAndSwapInt32(&m.state, 0, -1) {
        return true
    }

    // Slow way
    return m.lockST(ctx)
}
// RTryLock - try read locks mutex with context
func (m *RWTMutex) RTryLock(ctx context.Context) bool {
    k := atomic.LoadInt32(&m.state)
    if k >= 0 && atomic.CompareAndSwapInt32(&m.state, k, k+1) {
        return true
    }

    // Slow way
    return m.rlockST(ctx)
}


Como puede ver, si el Mutex no está bloqueado, entonces puede simplemente bloquearse, pero si no, pasaremos a un esquema más complejo.



Al principio, obtenemos un canal, y entramos en un bucle sin fin, si resultó estar bloqueado, salimos con éxito, y si no, entonces comenzamos a esperar uno de 2 eventos, o que el canal esté desbloqueado, o que el flujo ctx.Done () se desbloqueará:



func (m *RWTMutex) chGet() chan struct{} {
    m.mx.Lock()
    if m.ch == nil {
        m.ch = make(chan struct{}, 1)
    }
    r := m.ch
    m.mx.Unlock()
    return r
}

func (m *RWTMutex) lockST(ctx context.Context) bool {
    ch := m.chGet()
    for {
        if atomic.CompareAndSwapInt32(&m.state, 0, -1) {
            return true
        }
        if ctx == nil {
            return false
        }
        select {
        case <-ch:
            ch = m.chGet()
        case <-ctx.Done():
            return false
        }
    }
}

func (m *RWTMutex) rlockST(ctx context.Context) bool {
    ch := m.chGet()
    var k int32
    for {
        k = atomic.LoadInt32(&m.state)
        if k >= 0 && atomic.CompareAndSwapInt32(&m.state, k, k+1) {
            return true
        }
        if ctx == nil {
            return false
        }
        select {
        case <-ch:
            ch = m.chGet()
        case <-ctx.Done():
            return false
        }
    }
}


Desbloqueemos el mutex.



Necesitamos cambiar el estado y, si es necesario, desbloquear el canal.



Como escribí anteriormente, si el canal está cerrado, entonces case <-ch omitirá el flujo de ejecución más.



func (m *RWTMutex) chClose() {
    if m.ch == nil {
        return
    }

    var o chan struct{}
    m.mx.Lock()
    if m.ch != nil {
        o = m.ch
        m.ch = nil
    }
    m.mx.Unlock()
    if o != nil {
        close(o)
    }
}
// Unlock - unlocks mutex
func (m *RWTMutex) Unlock() {
    if atomic.CompareAndSwapInt32(&m.state, -1, 0) {
        m.chClose()
        return
    }

    panic("RWTMutex: Unlock fail")
}
// RUnlock - unlocks mutex
func (m *RWTMutex) RUnlock() {
    i := atomic.AddInt32(&m.state, -1)
    if i > 0 {
        return
    } else if i == 0 {
        m.chClose()
        return
    }

    panic("RWTMutex: RUnlock fail")
}


El mutex en sí está listo, debe escribir un par de pruebas y métodos estándar como Lock () y RLock () para ello



Los puntos de referencia de mi coche mostraron estas velocidades



  
BenchmarkRWTMutexTryLockUnlock-8        92154297                12.8 ns/op             0 B/op          0 allocs/op
BenchmarkRWTMutexTryRLockRUnlock-8      64337136                18.4 ns/op             0 B/op          0 allocs/op

 RWMutex
BenchmarkRWMutexLockUnlock-8            44187962                25.8 ns/op             0 B/op          0 allocs/op
BenchmarkRWMutexRLockRUnlock-8          94655520                12.6 ns/op             0 B/op          0 allocs/op

 Mutex
BenchmarkMutexLockUnlock-8              94345815                12.7 ns/op             0 B/op          0 allocs/op


Es decir, la velocidad de trabajo es comparable a la habitual RWMutex y Mutex.



Código en github



All Articles