Documentation ¶
Overview ¶
Package xsync provides thin wrappers around locking primitives in an effort towards better documenting the relationship between locks and the data they protect.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Once ¶
type Once[T any] struct { // contains filtered or unexported fields }
Once is a lock that ensures that some data is initialized exactly once.
Does not need explicit construction: simply do Once[MyType]{}.
func (*Once[T]) Get ¶
func (l *Once[T]) Get() *T
Get the previously initialized value.
If the Once is not yet initialized, nil is returned.
func (*Once[T]) GetOrInit ¶
GetOrInit the data protected by this lock.
If the init function fails, the error is returned and the data is still considered to be uninitialized. The init function will then be called again on the next GetOrInit call. Only one thread will ever call init at the same time.
type RWMutex ¶
type RWMutex[T any] struct { // contains filtered or unexported fields }
RWMutex is a thin wrapper around sync.RWMutex that hides away the data it protects to ensure it's not accidentally accessed without actually holding the lock.
The design is inspired by how Rust implement its locks.
Given Go's weak type system it's not able to provide perfect safety, but it at least clearly communicates to developers exactly which resources are protected by which lock without having to sift through documentation (or code, if documentation doesn't exist).
To better demonstrate how this abstraction helps to avoid mistakes, consider the following example struct implementing an object manager of some sort:
type ID uint64 type SomeObject struct { // ... } type ObjectManager struct { objects map[ID]*SomeObject } func (mgr *ObjectManager) AddObject(id ID, obj *SomeObject) { mgr.objects[id] = obj } func (mgr *ObjectManager) RemoveObject(id ID) { delete(mgr.objects, id) } func (mgr *ObjectManager) GetObject(id ID) *SomeObject { x := mgr.objects[id] return x }
Now you want to rework the public interface of ObjectManager to be thread-safe. The perhaps most obvious solution would be to just add `mu sync.RWMutex` to ObjectManager and lock it immediately when entering each public function:
type ID uint64 type SomeObject struct { // ... } type ObjectManager struct { mu sync.RWMutex objects map[ID]*SomeObject } func (mgr *ObjectManager) AddObject(id ID, obj *SomeObject) { mgr.mu.Lock() mgr.mu.Unlock() // <- oh no, forgot to write `defer`! mgr.objects[id] = obj } func (mgr *ObjectManager) RemoveObject(id ID) { // oh no, forgot to take the lock entirely! delete(mgr.objects, id) } func (mgr *ObjectManager) GetObject(id ID) *SomeObject { mgr.mu.RLock() defer mgr.mu.RUnlock() return mgr.objects[id] }
Unfortunately, we made two mistakes in our implementation. The code will however likely still pass all kinds of tests, simply because it's very hard to write tests that detect race conditions in tests.
Now, the same thing using xsync.RWMutex instead:
type ID uint64 type SomeObject struct { // ... } type ObjectManager struct { objects xsync.RWMutex[map[ID]*SomeObject] } func (mgr *ObjectManager) AddObject(id ID, obj *SomeObject) { var *SomeObject objects := mgr.objects.RLock() mgr.objects.RUnlock(&objects) // <- oh no, forgot to write `defer`! objects[id] = obj // <- will immediately crash in tests // because `RUnlock` set our pointer to `nil` } func (mgr *ObjectManager) RemoveObject(id ID) { // oh no, forgot to take the lock entirely! With xsync.RWMutex, this won't // compile: there simply is no direct pointer to the protected data that we // could use to accidentally access shared data without going through calling // `RLock`/`WLock` first. delete(mgr.objects, id) } func (mgr *ObjectManager) GetObject(id ID) *SomeObject { objects := mgr.mu.RLock() defer mgr.mu.RUnlock(&objects) return mgr.objects[id] }
func NewRWMutex ¶
NewRWMutex creates a new read-write mutex.
func (*RWMutex[T]) RLock ¶
func (mtx *RWMutex[T]) RLock() *T
RLock locks the mutex for reading, returning a pointer to the protected data.
The caller **must not** write to the data pointed to by the returned pointer.
Further, the caller **must not** let the returned pointer leak out of the scope of the function where it was originally created, except for temporarily borrowing it to other functions. The caller must make sure that callees never save this pointer anywhere.
func (*RWMutex[T]) RUnlock ¶
func (mtx *RWMutex[T]) RUnlock(ref **T)
RUnlock unlocks the mutex after previously being locked by RLock.
Pass a reference to the pointer returned from RLock here to ensure it is invalidated.
func (*RWMutex[T]) WLock ¶
func (mtx *RWMutex[T]) WLock() *T
WLock locks the mutex for writing, returning a pointer to the protected data.
The caller **must not** let the returned pointer leak out of the scope of the function where it was originally created, except for temporarily borrowing it to other functions. The caller must make sure that callees never save this pointer anywhere.
Example ¶
package main import ( "go.opentelemetry.io/ebpf-profiler/libpf/xsync" ) func main() { m := xsync.NewRWMutex(uint64(0)) p := m.WLock() *p = 123 // Copy the reference, defeating the pointer invalidation in `WUnlock. Do NOT do this. p2 := p m.WUnlock(&p) // We can incorrectly still write the data without holding the actual lock: *p2 = 345 }
Output: