xsync

package
v0.0.0-...-6ad74a7 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 28, 2024 License: Apache-2.0 Imports: 2 Imported by: 0

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

func (l *Once[T]) GetOrInit(init func() (T, error)) (*T, error)

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

func NewRWMutex[T any](guarded T) RWMutex[T]

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:

func (*RWMutex[T]) WUnlock

func (mtx *RWMutex[T]) WUnlock(ref **T)

WUnlock unlocks the mutex after previously being locked by WLock.

Pass a reference to the pointer returned from WLock here to ensure it is invalidated.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL