Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type BravoRWMutex ¶
type BravoRWMutex struct {
// contains filtered or unexported fields
}
BravoRWMutex is a reader/writer mutual exclusion lock with the method set of sync.RWMutex: the lock can be held by an arbitrary number of readers or a single writer, the zero value is an unlocked mutex ready for use, and it must not be copied after first use.
It has one semantic deviation from sync.RWMutex, and it is not a small one. See "Deviations" below before using this type.
Algorithm ¶
BravoRWMutex implements BRAVO - "Biased Locking for Reader-Writer Locks", by Dave Dice and Alex Kogan (USENIX ATC '19). See REFERENCES.md in this directory for the full citation.
BRAVO is less a lock than a reader-scalability layer bolted onto one. BravoRWMutex embeds an ordinary sync.RWMutex and, while the lock is in "biased" mode, lets readers bypass it entirely:
- A reader hashes the lock's address together with its own goroutine identity to a slot in a process-wide table and claims the slot with a single CAS. A successful claim is the read lock; the embedded RWMutex, and its centrally contended reader counter, are never touched.
- A writer takes the embedded RWMutex, clears the bias flag so no further slots can be claimed, and then waits for the slots belonging to this lock to empty.
The distinctive part is what happens next. Having paid to revoke bias, the writer measures how long that took and inhibits bias for a multiple of that duration. During the inhibit period readers fall back to the embedded RWMutex, so a write-heavy phase degrades to plain sync.RWMutex behavior instead of making every writer pay a table scan. When writes subside, the first slow-path reader past the deadline re-enables bias. The lock tunes itself; callers need not know the read/write ratio in advance.
Why this over a distributed reader count ¶
A per-P or per-node reader count (RWMutex, CohortRWMutex, Linux's percpu_rw_semaphore) costs O(GOMAXPROCS) memory *per lock instance*. That is affordable for a handful of hot singletons and ruinous for a program holding many - which is why Linux restricts percpu_rw_semaphore to a few global locks. BRAVO amortizes one shared table across every lock in the process, so a BravoRWMutex costs a few words no matter how many exist. That, not raw read throughput, is the property it exists to provide, and [BenchmarkRWMutexManyLocks] is where to look for it.
Deviations from sync.RWMutex ¶
**RUnlock must be called from the goroutine that called RLock.** sync.RWMutex explicitly permits releasing a read lock from a different goroutine; BravoRWMutex does not, and doing so will panic (or, worse, misbehave) rather than work. Everything else - writer preference, migration between Ps, the zero value, TryLock/TryRLock - matches.
The restriction is inherent, not an oversight. BRAVO's read-unlock in the paper consumes a token returned by its read-lock, saying which path the reader took. RUnlock takes no arguments, so the token has to come from somewhere else, and goroutine identity is the only thing available - which is precisely what forbids releasing from another goroutine. Inferring the path instead (release a slot if one exists, else the underlying lock) makes RUnlock a guess between two disjoint reader populations; that guess is count-preserving in aggregate but not exact per reader, and an earlier prototype of this type doing exactly that deadlocked under benchmark load. Making releases interchangeable so that any goroutine can retire any reader's registration removes the need for a token altogether - and that is C-SNZI, which this package provides as SNZIRWMutex.
For the same reason as the other implementations here, an unbalanced RUnlock is not detected.
Java reached the same fork and resolved it the same way: StampedLock returns a stamp from every acquire and requires it back on release, and pays for that by not implementing ReadWriteLock and not being reentrant. Rust does not face the fork at all, because RwLockReadGuard is RAII and the guard can carry the token implicitly.
Portability ¶
The goroutine token comes from the g pseudo-register via a two-instruction assembly function; see goToken. On architectures without one the token is 0, the biased fast path is disabled entirely, and BravoRWMutex degenerates to a thin wrapper over sync.RWMutex - slower, but correct, and with the cross-goroutine restriction lifted since there is no fast path to attribute.
Memory model ¶
In the terminology of the Go memory model, the n'th call to BravoRWMutex.Unlock "synchronizes before" the m'th call to Lock for any n < m, just as for sync.Mutex. For any call to RLock, there exists an n such that the n'th call to Unlock "synchronizes before" that call to RLock, and the corresponding call to BravoRWMutex.RUnlock "synchronizes before" the n+1'th call to Lock.
func (*BravoRWMutex) Lock ¶
func (rw *BravoRWMutex) Lock()
Lock locks rw for writing. If the lock is already locked for reading or writing, Lock blocks until the lock is available.
func (*BravoRWMutex) RLock ¶
func (rw *BravoRWMutex) RLock()
RLock locks rw for reading.
It should not be used for recursive read locking; a blocked Lock call excludes new readers from acquiring the lock. See the documentation on the BravoRWMutex type, in particular the requirement that RUnlock be called from this same goroutine.
func (*BravoRWMutex) RLocker ¶
func (rw *BravoRWMutex) RLocker() sync.Locker
RLocker returns a sync.Locker interface that implements the Lock and Unlock methods by calling rw.RLock and rw.RUnlock.
func (*BravoRWMutex) RUnlock ¶
func (rw *BravoRWMutex) RUnlock()
RUnlock undoes a single RLock call; it does not affect other simultaneous readers.
Unlike sync.RWMutex.RUnlock, this must be called from the goroutine that called RLock; see the type documentation. It is a run-time error if rw is not locked for reading on entry to RUnlock, but as with the other implementations in this package that case is not detected.
func (*BravoRWMutex) TryLock ¶
func (rw *BravoRWMutex) TryLock() bool
TryLock tries to lock rw for writing and reports whether it succeeded.
Note that while correct uses of TryLock do exist, they are rare, and use of TryLock is often a sign of a deeper problem in a particular use of mutexes.
func (*BravoRWMutex) TryRLock ¶
func (rw *BravoRWMutex) TryRLock() bool
TryRLock tries to lock rw for reading and reports whether it succeeded.
Note that while correct uses of TryRLock do exist, they are rare, and use of TryRLock is often a sign of a deeper problem in a particular use of mutexes.
func (*BravoRWMutex) Unlock ¶
func (rw *BravoRWMutex) Unlock()
Unlock unlocks rw for writing. It is a run-time error if rw is not locked for writing on entry to Unlock; this is enforced by the embedded sync.RWMutex.
As with sync.RWMutex, a locked BravoRWMutex is not associated with a particular goroutine, so one goroutine may Lock it and arrange for another to Unlock it. That freedom applies to the write lock only; see RUnlock.
type CohortRWMutex ¶
type CohortRWMutex struct {
// contains filtered or unexported fields
}
CohortRWMutex is a reader/writer mutual exclusion lock with the same semantics and method set as sync.RWMutex: the lock can be held by an arbitrary number of readers or a single writer, the zero value is an unlocked mutex ready for use, and it must not be copied after first use.
Algorithm ¶
CohortRWMutex implements C-RW-WP, the writer-preference member of the NUMA-aware reader-writer lock family introduced by Irina Calciu, Dave Dice, Yossi Lev, Victor Luchangco, Virendra Marathe and Nir Shavit in "NUMA-Aware Reader-Writer Locks" (PPoPP '13), which builds the writer side out of the lock cohorting technique of Dice, Marathe and Shavit ("Lock Cohorting: A General Technique for Designing NUMA Locks", PPoPP '12). See REFERENCES.md in this directory for full citations.
The design has two halves, and they address different costs.
The read side is a distributed reader indicator with one counter per group rather than one per P. Grouping matters in both directions: the counter's cache line stays within a group, so readers on different groups never contend, while a writer's drain scans only GOMAXPROCS/4 counters instead of GOMAXPROCS of them. Per-node indicators are the sweet spot on real hardware, because the coherence traffic worth eliminating is the cross-interconnect kind - intra-socket line sharing is an order of magnitude cheaper.
The write side is a cohort lock: a global mutex plus a per-group mutex. A writer releasing the lock hands it directly to another writer in its own group, if one is waiting, without releasing the global mutex - up to cohortMaxBatch consecutive handoffs. This is the half that actually carries the NUMA win, and it is not about the lock word: a run of same-group writers keeps the *protected data's* working set resident in one cache hierarchy instead of dragging every modified line across the interconnect on each handoff.
WP is writer preference. Readers check a writer-present flag after incrementing their counter and back out if a writer has announced itself, so a blocked Lock excludes new readers.
The Go caveat ¶
Go exposes no NUMA topology. There is no node ID, exported or internal, and a P has no stable affinity to a core, let alone a socket: the runtime hands Ps between Ms and the OS migrates Ms freely. This implementation therefore groups by P index, as a proxy: with cohortGroupSize at 4, Ps 0-3 form one group, 4-7 the next, and so on. The proxy makes the cohort handoff structurally correct and narrows the writer's scan, but it cannot guarantee that a group corresponds to a socket, so the cache-locality benefit - the half that actually carries the NUMA win - is opportunistic rather than assured. On Linux one could do better with getcpu(2) via the vDSO plus /sys/devices/system/node, at the cost of portability and of fighting P-to-M-to-CPU churn.
Consequently, on the single-socket hardware this package has been measured on, CohortRWMutex has nothing to offer over RWMutex or SNZIRWMutex: see the sweep recorded on cohortGroupSize, where the best-measuring configuration is precisely the one that switches C-RW-WP's distinguishing machinery off. Treat this implementation as untested rather than as evaluated: the design targets multi-socket machines, and the result that would justify it cannot be produced on one socket.
Memory model ¶
In the terminology of the Go memory model, the n'th call to CohortRWMutex.Unlock "synchronizes before" the m'th call to Lock for any n < m, just as for sync.Mutex. For any call to RLock, there exists an n such that the n'th call to Unlock "synchronizes before" that call to RLock, and the corresponding call to CohortRWMutex.RUnlock "synchronizes before" the n+1'th call to Lock.
Deviations from sync.RWMutex ¶
Unlike sync.RWMutex, CohortRWMutex does not detect and report an unbalanced RUnlock as a run-time error. An unbalanced RUnlock is a programming error and its effect is undefined. Unbalanced Unlock calls are still reported, because they are caught by the underlying sync.Mutex.
func (*CohortRWMutex) Lock ¶
func (rw *CohortRWMutex) Lock()
Lock locks rw for writing. If the lock is already locked for reading or writing, Lock blocks until the lock is available.
func (*CohortRWMutex) RLock ¶
func (rw *CohortRWMutex) RLock()
RLock locks rw for reading.
It should not be used for recursive read locking; a blocked Lock call excludes new readers from acquiring the lock. See the documentation on the CohortRWMutex type.
func (*CohortRWMutex) RLocker ¶
func (rw *CohortRWMutex) RLocker() sync.Locker
RLocker returns a sync.Locker interface that implements the Lock and Unlock methods by calling rw.RLock and rw.RUnlock.
func (*CohortRWMutex) RUnlock ¶
func (rw *CohortRWMutex) RUnlock()
RUnlock undoes a single RLock call; it does not affect other simultaneous readers. RUnlock need not be called by the same goroutine that called RLock, matching sync.RWMutex's documented allowance for Lock/Unlock. It is a run-time error if rw is not locked for reading on entry to RUnlock, but see the type documentation: unlike sync.RWMutex, CohortRWMutex does not detect this case.
func (*CohortRWMutex) TryLock ¶
func (rw *CohortRWMutex) TryLock() bool
TryLock tries to lock rw for writing and reports whether it succeeded.
Note that while correct uses of TryLock do exist, they are rare, and use of TryLock is often a sign of a deeper problem in a particular use of mutexes.
func (*CohortRWMutex) TryRLock ¶
func (rw *CohortRWMutex) TryRLock() bool
TryRLock tries to lock rw for reading and reports whether it succeeded.
Note that while correct uses of TryRLock do exist, they are rare, and use of TryRLock is often a sign of a deeper problem in a particular use of mutexes.
func (*CohortRWMutex) Unlock ¶
func (rw *CohortRWMutex) Unlock()
Unlock unlocks rw for writing. It is a run-time error if rw is not locked for writing on entry to Unlock; this is enforced by the underlying sync.Mutex.
As with sync.RWMutex, a locked CohortRWMutex is not associated with a particular goroutine. One goroutine may CohortRWMutex.Lock a CohortRWMutex and then arrange for another goroutine to Unlock it.
type Counter ¶
Counter is an int64, uint64, or float64 counter that is optimized for concurrent use. It is optimized for frequent writes and relatively infrequent reads. Writes are simple updates and remain fast even under many concurrent operations. Reads are slower and take time proportional to GOMAXPROCS.
The zero value is a valid, ready-to-use Counter. A Counter must not be copied after first use.
func (*Counter[T]) Add ¶
func (c *Counter[T]) Add(delta T)
Add adds delta to the counter.
Add does NOT establish a happens-before relationship between the calling goroutine and other goroutines executing Add or Value. Callers must not rely on Add as a memory barrier or synchronization primitive to publish or synchronize access to other non-counter memory mutations. Overflow wraps around.
func (*Counter[T]) Value ¶
func (c *Counter[T]) Value() T
Value returns the current total value of the counter. Value takes time proportional to GOMAXPROCS.
The value returned by Value should be considered approximate while there are concurrent writers: it sums the per-P shards one at a time without pausing writers, so the total need not equal the counter's value at any single instant. It is exact only if the caller can guarantee there are no concurrent writers to the counter.
type RWMutex ¶
type RWMutex struct {
// contains filtered or unexported fields
}
RWMutex is a reader/writer mutual exclusion lock. Like sync.RWMutex, the lock can be held by an arbitrary number of readers or a single writer, the zero value is an unlocked mutex ready for use, and a RWMutex must not be copied after first use.
If any goroutine calls RWMutex.Lock while the lock is already held by one or more readers, concurrent calls to RWMutex.RLock will block until the writer has acquired (and released) the lock, to ensure that the lock eventually becomes available to the writer. As with sync.RWMutex, this prohibits recursive read-locking, and a RLock cannot be upgraded into a Lock, nor can a Lock be downgraded into a RLock.
sync.RWMutex tracks readers with a single atomic counter, so every RLock and RUnlock, regardless of which core or NUMA node it runs on, performs an atomic read-modify-write on the same cache line. Under heavy read concurrency that cache line is exactly the kind of centrally contended, coherence-bounced state that grows more expensive as core and NUMA-node count grows (see Hardware and its Habits, and the reader-writer locking progression in Locking, in Paul McKenney's "Is Parallel Programming Hard, And, If So, What Can You Do About It?").
RWMutex instead shards the reader count across one counter per P (see GOMAXPROCS), using the same per-P striping Counter uses. RLock and RUnlock only ever touch the shard for the P they happen to run on, so concurrent readers never contend with each other, at the cost of Lock having to add up every shard: Lock is O(GOMAXPROCS) instead of O(1), and the lock occupies O(GOMAXPROCS) memory instead of a handful of words. This is the classic per-CPU reader-writer lock trade-off (Linux calls the analogous primitive percpu_rw_semaphore; folly calls it SharedMutex), sitting between a single shared-counter RWMutex and RCU: it keeps RWMutex's exact exclusion semantics (a reader never runs concurrently with a writer's critical section) rather than RCU's weaker "readers may see an old or new but always consistent version" contract, which requires redesigning the protected data structure around copy-and-publish. Prefer this RWMutex over sync.RWMutex when profiling shows RLock/RUnlock contention on a large or many-node machine; prefer sync.RWMutex otherwise, since it is smaller, and its Lock is cheaper.
In the terminology of the Go memory model, the n'th call to RWMutex.Unlock "synchronizes before" the m'th call to Lock for any n < m, just as for sync.Mutex. For any call to RLock, there exists an n such that the n'th call to Unlock "synchronizes before" that call to RLock, and the corresponding call to RWMutex.RUnlock "synchronizes before" the n+1'th call to Lock.
Unlike sync.RWMutex, RWMutex does not detect and report unbalanced RUnlock calls (an RUnlock with no matching RLock) as a run-time error; doing so would require the same precise, centrally-visible reader accounting the sharded design exists to avoid. An unbalanced RUnlock is a programming error, and its effect on RWMutex is undefined. Unbalanced Unlock calls are still reported, because they are caught by the embedded writer sync.Mutex.
func (*RWMutex) Lock ¶
func (rw *RWMutex) Lock()
Lock locks rw for writing. If the lock is already locked for reading or writing, Lock blocks until the lock is available.
Correctness note: sync/atomic operations are sequentially consistent (https://go.dev/ref/mem), so there is a single total order over every shardAdd and every access of writing and notify below. Consider a reader whose RLock call takes the fast path, i.e. observes writing == false. Its shardAdd(shard, 1) precedes that load of writing in program order. If that load returns false, it must precede this method's writing.Store(true) in the total order (a Load can only return a value written before it in the order, and writing has no other writer while rw.w is held). Chaining program order and the total order gives: the reader's shardAdd(shard, +1) precedes this writing.Store, which precedes every sumShards call below in program order, which precedes them in the total order too. So every reader that successfully took the fast path is counted by some sumShards call this method makes, and the drain loop below cannot return before that reader's eventual RUnlock. (This is the same "flag observed then counter" argument Linux's percpu_rw_semaphore relies on, but Linux needs an explicit synchronize_rcu to get it, because the kernel's base memory model is far weaker than sequential consistency.)
func (*RWMutex) RLock ¶
func (rw *RWMutex) RLock()
RLock locks rw for reading.
It should not be used for recursive read locking; a blocked Lock call excludes new readers from acquiring the lock. See the documentation on the RWMutex type.
func (*RWMutex) RLocker ¶
RLocker returns a sync.Locker interface that implements the Lock and Unlock methods by calling rw.RLock and rw.RUnlock.
func (*RWMutex) RUnlock ¶
func (rw *RWMutex) RUnlock()
RUnlock undoes a single RLock call; it does not affect other simultaneous readers. RUnlock need not be called by the same goroutine that called RLock, matching sync.RWMutex.Unlock's documented allowance for Lock/Unlock. It is a run-time error if rw is not locked for reading on entry to RUnlock, but see the type documentation: unlike sync.RWMutex, RWMutex does not detect this case.
func (*RWMutex) TryLock ¶
TryLock tries to lock rw for writing and reports whether it succeeded.
Note that while correct uses of TryLock do exist, they are rare, and use of TryLock is often a sign of a deeper problem in a particular use of mutexes.
func (*RWMutex) TryRLock ¶
TryRLock tries to lock rw for reading and reports whether it succeeded.
Note that while correct uses of TryRLock do exist, they are rare, and use of TryRLock is often a sign of a deeper problem in a particular use of mutexes.
func (*RWMutex) Unlock ¶
func (rw *RWMutex) Unlock()
Unlock unlocks rw for writing. It is a run-time error if rw is not locked for writing on entry to Unlock; this is enforced by the embedded writer sync.Mutex.
As with sync.RWMutex, a locked RWMutex is not associated with a particular goroutine. One goroutine may RWMutex.Lock a RWMutex and then arrange for another goroutine to Unlock it.
type SNZIRWMutex ¶
type SNZIRWMutex struct {
// contains filtered or unexported fields
}
SNZIRWMutex is a reader/writer mutual exclusion lock with the same semantics and method set as sync.RWMutex: the lock can be held by an arbitrary number of readers or a single writer, the zero value is an unlocked mutex ready for use, and it must not be copied after first use.
Algorithm ¶
SNZIRWMutex tracks readers with a closable scalable nonzero indicator (C-SNZI), the primitive introduced by Yossi Lev, Victor Luchangco and Marek Olszewski in "Scalable Reader-Writer Locks" (SPAA '09), which is in turn a closable variant of the scalable nonzero indicator (SNZI) of Faith Ellen, Yossi Lev, Victor Luchangco and Mark Moir ("SNZI: Scalable Nonzero Indicators", PODC '07). See REFERENCES.md in this directory for full citations.
The founding observation is that a reader-writer lock never needs to know how many readers there are - only whether there are any. Maintaining an exact count is what forces a distributed reader-count design (see RWMutex, and the per-thread-lock scheme of Hsieh and Weihl that Linux's brlock and lglock implement) to make the writer sum every shard, so that acquiring the write lock costs O(GOMAXPROCS) cache misses. An indicator that answers only "is the surplus nonzero?" can be arranged as a hierarchy whose root answers that question in O(1).
This implementation uses the two-level form of that hierarchy: one leaf per P (reusing the same cache-line-aligned per-P striping as Counter), and a single root word.
- A reader arriving at a leaf whose count is already nonzero simply increments the leaf. Nothing outside that P's cache line is touched.
- A reader arriving at an empty leaf must first register the leaf with the root, so a 0 -> nonzero leaf transition costs one root CAS.
- Departures mirror this: a leaf returning to zero deregisters from the root.
The root therefore counts nonempty leaves (plus arrivals in flight), and its surplus is zero exactly when no reader holds the lock. A writer reads one word instead of GOMAXPROCS of them.
"Closable" is the writer's drain protocol, and it replaces the separate writer-announcement flag that RWMutex uses. SNZIRWMutex.Lock sets the closed bit in the same word as the surplus, in a single CAS, so the flag a reader must consult and the count a writer must drain are the same word and cannot disagree.
A reader arriving at an empty leaf is refused outright, because registering with the root is itself the CAS that observes the closed bit. A reader arriving at an already-registered leaf never touches the root, so it re-reads the root once after incrementing its leaf and backs out if a writer has closed the indicator in the meantime. That read costs a shared-state cache hit, not a coherence miss, since the root is written only on leaf transitions.
What this design does *not* need is a per-departure wakeup. A draining writer polls one word, so departing readers signal nothing at all; only a writer's release touches the channel that parks excluded readers, once.
Trade-offs ¶
Relative to RWMutex, SNZIRWMutex moves cost from the writer to the reader. Lock is O(1) rather than O(GOMAXPROCS). In exchange, a reader whose leaf is empty pays a CAS on the shared root word, so a workload of brief, non-overlapping readers - each RLock finding its leaf at zero - touches the root on every acquisition and can be slower than RWMutex. SNZIRWMutex is at its best when read-side critical sections overlap, which keeps leaves nonempty and root traffic near zero. This is the trade-off Paul McKenney summarizes as "give up some update-side performance" in Quick Quiz 5.65 of "Is Parallel Programming Hard, And, If So, What Can You Do About It?" - except that here it is the read side that gives ground, and only when readers do not overlap.
Like RWMutex and sync.RWMutex, a blocked Lock excludes new readers, so the lock cannot be recursively read-locked and an RLock cannot be upgraded to a Lock.
Memory model ¶
In the terminology of the Go memory model, the n'th call to SNZIRWMutex.Unlock "synchronizes before" the m'th call to Lock for any n < m, just as for sync.Mutex. For any call to RLock, there exists an n such that the n'th call to Unlock "synchronizes before" that call to RLock, and the corresponding call to SNZIRWMutex.RUnlock "synchronizes before" the n+1'th call to Lock.
Deviations from sync.RWMutex ¶
Unlike sync.RWMutex, SNZIRWMutex does not detect and report an unbalanced RUnlock (an RUnlock with no matching RLock) as a run-time error. An unbalanced RUnlock is a programming error and its effect is undefined. Unbalanced Unlock calls are still reported, because they are caught by the embedded writer sync.Mutex.
func (*SNZIRWMutex) Lock ¶
func (rw *SNZIRWMutex) Lock()
Lock locks rw for writing. If the lock is already locked for reading or writing, Lock blocks until the lock is available.
Lock is O(1) in GOMAXPROCS: it reads the single root word rather than summing per-P state.
func (*SNZIRWMutex) RLock ¶
func (rw *SNZIRWMutex) RLock()
RLock locks rw for reading.
It should not be used for recursive read locking; a blocked Lock call excludes new readers from acquiring the lock. See the documentation on the SNZIRWMutex type.
func (*SNZIRWMutex) RLocker ¶
func (rw *SNZIRWMutex) RLocker() sync.Locker
RLocker returns a sync.Locker interface that implements the Lock and Unlock methods by calling rw.RLock and rw.RUnlock.
func (*SNZIRWMutex) RUnlock ¶
func (rw *SNZIRWMutex) RUnlock()
RUnlock undoes a single RLock call; it does not affect other simultaneous readers. RUnlock need not be called by the same goroutine that called RLock, matching sync.RWMutex's documented allowance for Lock/Unlock. It is a run-time error if rw is not locked for reading on entry to RUnlock, but see the type documentation: unlike sync.RWMutex, SNZIRWMutex does not detect this case.
func (*SNZIRWMutex) TryLock ¶
func (rw *SNZIRWMutex) TryLock() bool
TryLock tries to lock rw for writing and reports whether it succeeded.
Note that while correct uses of TryLock do exist, they are rare, and use of TryLock is often a sign of a deeper problem in a particular use of mutexes.
func (*SNZIRWMutex) TryRLock ¶
func (rw *SNZIRWMutex) TryRLock() bool
TryRLock tries to lock rw for reading and reports whether it succeeded.
Note that while correct uses of TryRLock do exist, they are rare, and use of TryRLock is often a sign of a deeper problem in a particular use of mutexes.
func (*SNZIRWMutex) Unlock ¶
func (rw *SNZIRWMutex) Unlock()
Unlock unlocks rw for writing. It is a run-time error if rw is not locked for writing on entry to Unlock; this is enforced by the embedded writer sync.Mutex.
As with sync.RWMutex, a locked SNZIRWMutex is not associated with a particular goroutine. One goroutine may SNZIRWMutex.Lock a SNZIRWMutex and then arrange for another goroutine to Unlock it.