¡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.
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.