Olá!
Neste artigo, gostaria de dizer como você poderia fazer seu próprio RWMutex, mas com a capacidade de definir o tempo limite ou acionar o contexto para ignorar o bloqueio. Ou seja, implemente TryLock (context.Context) e RExperimenteLock (context.Context), mas para seu próprio Mutex.

A imagem mostra como despejar água em um gargalo muito estreito.
Para começar, deve ser esclarecido que para 99% das tarefas, tais métodos não são necessários. Eles são necessários quando o recurso bloqueado pode não ser liberado por um longo tempo. Gostaria de ressaltar que se um recurso bloqueado permanecer ocupado por muito tempo, vale a pena tentar otimizar a lógica no início de forma a minimizar o tempo de bloqueio.
Para obter mais informações, consulte Dançando com Mutexes em Go no Exemplo 2.
Mas se, no entanto, temos que ter uma longa retenção de um fluxo de recursos, então me parece que será difícil prescindir do 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 você pode ver, se o Mutex não estiver bloqueado, ele pode ser simplesmente bloqueado, mas se não, passaremos para um esquema mais complexo.
No começo, pegamos o canal, e entramos em um loop infinito, se estiver travado, saímos com sucesso, e se não, então começamos a esperar por um dos 2 eventos, ou que o canal está desbloqueado, ou que o stream ctx.Done () irá 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
}
}
}
Vamos desbloquear o mutex.
Precisamos mudar o estado e, se necessário, desbloquear o canal.
Como escrevi acima, se o canal for fechado, o caso <-ch irá ignorar o fluxo de execução.
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")
}
O mutex em si está pronto, você precisa escrever alguns testes e métodos padrão como Lock () e RLock () para ele
Os benchmarks do meu carro mostraram essas 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
Ou seja, a velocidade de trabalho é comparável ao RWMutex e Mutex usuais.