mutex

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Apr 1, 2024 License: MIT Imports: 1 Imported by: 1

README

Mutex: Value, Numeric, and Map using Generics

Test Go Report Card Go Reference Release Dependencies

Package mutex provides a collection of thread-safe data structures using generics in Go.

It offers a Value type for lock-protected values, a Numeric type for thread-safe numeric operations, and a Map type for a concurrent map with type safety. These structures are designed to be easy to use, providing a simple and familiar interface similar to well known atomic.Value and sync.Map, but with added type safety and the flexibility of generics.

The package aims to simplify concurrent programming by ensuring safe access to shared data and reducing the boilerplate code associated with mutexes.

  • Value, Numeric implements a simple thread-safe Value store that behaves similarly to atomic.Value but uses sync.RWMutex instead.
  • Numeric extends Value with the Add(delta V) V function to simplify thread-safe counters.
  • Map implements a simple thread-safe map that behaves similarly to sync.Map adding type safety and making it simple to know how many unique keys are in the map.

Data Types

Value and Numeric use generics, in general it is recommended to use them with built-in types, and they work best if you don't use of pointers (see below for more information about pointer values).

This means that when initialising a Value or Numeric a new variable will be created to hold the protected value.

For these reasons NewMapWithValue copies the map to the lock-protected map.

Please note that Map, Value, Numeric use reflect.DeepEqual in comparisons.

About Load() and Exclusive()

When loading a value using Load() you have no guarantee that that value won't be changed when calling any Set method. This means that, similar to atomic.Value, you should not use mutex.Value or mutex.Numeric to develop a synchronisation algorithm unless you clearly understand the implications of doing so. The same goes for Map values when using the Load(K) V,bool method.

When it is important to maintain consistency between read and write use Exclusive() that locks the value while the function is executed. For details on Exclusive argument review the relative interface.

Pointers Values, Maps and Slices

Consider the following when using Value and Numeric with pointer values, maps and slices:

  • Concurrent Modification: If multiple goroutines modify the data pointed to by the same pointer without proper synchronisation, it can lead to race conditions and unpredictable behaviour.
  • Data Race: Even if Value itself is thread-safe, the data pointed to by the values is not automatically protected. Accessing or modifying the data through pointers in concurrent goroutines can cause data races.

Documentation

You can find the generated go doc here.

Key Features

Map, Value and Numeric:

  • Type Safety: Uses generics to provide a type-safe, lock-protected access to values.
  • Thread-Safety: Ensures safe concurrent access to the value through the use of a sync.RWMutex.
  • Atomic Updates: Includes functions that allows for atomic modifications to values in the map.
  • Shortcut: Use a simple zero-dependency package to avoid rewriting the same code around mutex value protection over and over.

Map:

  • Iteration: Supports iterating over the map with the Range function, and provides methods to obtain slices of keys (Keys), values (Values), or both (Entries).
  • Map Size: Offers a Len function to easily retrieve the number of items in the map.

Motivation

During the implementation of TypedMap I originally used (or rather misused) atomic.Value, a more in-depth review of the code and articles on the subject, made it clear that my use case did not justify the use of atomic. Additionally, citing Go's atomic package:

These functions require great care to be used correctly. Except for special, low-level applications, synchronisation is better done with channels or the facilities of the sync package.

I did however liked how easy to use atomic.Value is despite the in-depth know how they required to be properly used, Value and Numeric aim to provide the same simplicity and with a similar interface. The same goes for sync.Map interface.

Usage

You can find more examples in example_test.go

package main

import (
	"fmt"

	"github.com/thetechpanda/mutex"
)

func main() {
	value_example()
	numeric_example()
	map_example()
}

func value_example() {
	m := mutex.NewValue[string]()
	value := "42"
	m.Store(value)
	v, ok := m.Load()
	fmt.Println("value =", v, ", ok =", ok) // value = 42 , ok = true
}

func numeric_example() {
	m := mutex.NewNumeric[int]()
	value := 42
	m.Store(value)
	v, ok := m.Load()
	fmt.Println("value =", v, ", ok =", ok) // value = 42 , ok = true
}

func map_example() {
	m := mutex.NewMap[string, int]()
	m.Store("key", 42)
	v, ok := m.Load("key")
	fmt.Println("value =", v, ", ok =", ok) // value = 42 , ok = true
}

Code coverage

$ go test -cover ./...
ok      github.com/thetechpanda/mutex   0.119s  coverage: 100.0% of statements
ok      github.com/thetechpanda/mutex/internal  0.443s  coverage: 100.0% of statements

Installation

go get github.com/thetechpanda/mutex

Bibliography

Below is a list of articles and videos that I reviewed to conclude that atomic operations were not the appropriate solution for the problem I was addressing. Some of these links provide documentation on how atomic operations work, while others discuss the nuances of using atomic operations effectively and sensibly.

Why most of the bibliography is about C++?

Reading and studying atomic operations in C++ can be applicable to Go because both languages deal with concurrency and share similar challenges related to memory consistency and synchronization. Understanding atomic operations in C++ can provide a deeper insight into low-level concurrency mechanisms, which can be beneficial when working with Go's concurrency primitives and atomic package, despite the differences in syntax and language features.

Disclaimer

The content provided in the above links is not written or curated by the author of this package. All rights and attributions belong to the original authors. The inclusion of these links does not imply any endorsement or ownership by the author of this package.

Roadmap

  • Mutex Slices
  • Benchmarks

Contributing

Contributions are welcome and very much appreciated!

Feel free to open an issue or submit a pull request.

License

The mutex package is released under the MIT License. See the LICENSE file for details.

Documentation

Overview

Mutex: Value, Numeric, and Map using Generics

Package mutex provides a collection of thread-safe data structures using generics in Go. It offers a Value type for lock-protected values, a Numeric type for thread-safe numeric operations, and a Map type for a concurrent map with type safety. These structures are designed to be easy to use, providing a simple and familiar interface similar to well known atomic.Value and sync.Map, but with added type safety and the flexibility of generics. The package aims to simplify concurrent programming by ensuring safe access to shared data and reducing the boilerplate code associated with mutexes.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Map added in v0.1.0

type Map[K comparable, V any] interface {
	// Load returns the value stored in the map for a key, or nil if no
	// value is present.
	// The ok result indicates whether value was found in the map.
	Load(key K) (v V, ok bool)
	// Store sets the value for a key.
	Store(key K, value V)
	// LoadOrStore returns the existing value for the key if present.
	// Otherwise, it stores and returns the given value.
	// The loaded result is true if the value was loaded, false if stored.
	LoadOrStore(key K, value V) (actual V, loaded bool)
	// LoadAndDelete deletes the value for a key, returning the previous value if any.
	// The loaded result reports whether the key was present.
	LoadAndDelete(key K) (value V, loaded bool)
	// Delete deletes the value for a key.
	Delete(key K)
	// Swap swaps the value for a key and returns the previous value if any.
	// The loaded result reports whether the key was present.
	Swap(key K, value V) (previous V, loaded bool)
	// CompareAndSwap swaps the old and new values for key
	// if the value stored in the map is equal to old.
	//
	// Returns true if the swap was performed.
	//
	// ! this function uses reflect.DeepEqual to compare the values.
	CompareAndSwap(key K, old, new V) bool
	// CompareAndDelete deletes the entry for key if its value is equal to old.
	//
	// If there is no current value for key in the map, CompareAndDelete
	// returns false (even if the old value is the nil interface value).
	//
	// ! this function uses reflect.DeepEqual to compare the values.
	CompareAndDelete(key K, old V) (deleted bool)
	// Range calls f sequentially for each key and value present in the map.
	// If f returns false, range stops the iteration.
	//
	// Range does not necessarily correspond to any consistent snapshot of the Map's
	// contents: no key will be visited more than once, but if the value for any key
	// is stored or deleted concurrently (including by f), Range may reflect any
	// mapping for that key from any point during the Range call. Range does not
	// block other methods on the receiver; even f itself may call any method on m.
	//
	// Range may be O(N) with the number of elements in the map even if f returns
	// false after a constant number of calls.
	Range(f func(K, V) bool)
	// Update allows the caller to change the value associated with the key atomically guaranteeing that the value would not be changed by another goroutine during the operation.
	//
	// ! Do not invoke any Map functions within 'f' to prevent a deadlock.
	Update(key K, f func(V, bool) V)
	// UpdateRange is a thread-safe version of Range that locks the map for the duration of the iteration and allows for the modification of the values.
	// If f returns false, UpdateRange stops the iteration, without updating the corresponding value in the map.
	//
	// ! Do not invoke any Map functions within 'f' to prevent a deadlock.
	UpdateRange(f func(K, V) (V, bool))
	// Exclusive provides a way to perform  operations on the map ensuring that no other operation is performed on the map during the execution of the function.
	//
	// ! Do not invoke any Map functions within 'f' to prevent a deadlock.
	Exclusive(f func(m map[K]V))
	// Clear removes all items from the map.
	Clear()
	// Has returns true if the map contains the key.
	Has(key K) bool
	// Keys returns a slice of all the keys present in the map, an empty slice is returned if the map is empty.
	Keys() (keys []K)
	// Values returns a slice of all the values present in the map, an empty slice is returned if the map is empty.
	Values() (values []V)
	// Entries returns two slices, one containing all the keys and the other containing all the values present in the map.
	Entries() (keys []K, values []V)
	// Len returns the number of unique keys in the map.
	Len() (n int)
}

Map is a generic interface that provides a way to interact with the map. its interface is identical to sync.Map and so are function definition and behaviour.

Example
package main

import (
	"fmt"
	"strconv"
	"sync"

	"github.com/thetechpanda/mutex"
)

func main() {
	m := mutex.NewMap[string, int]()
	// create a wait group to synchronize goroutines
	var wg sync.WaitGroup

	// the following two goroutine will store values from 0 to 999 using the keys "0" to "999"
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 1000; i++ {
			m.Store(strconv.Itoa(i), i)
		}
	}()

	// the following two goroutine will store values from 0 to -999 using the keys "0" to "999"
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 1000; i++ {
			m.Store(strconv.Itoa(i), -i)
		}
	}()

	// wait for goroutines to finish
	wg.Wait()

	v, ok := m.Load("123")
	// v is int
	fmt.Println("v =", v, ", ok =", ok) // "v = -123 , ok = true" or "v = 123 , ok = true"
}
Output:

func NewMap added in v0.1.0

func NewMap[K comparable, V any]() Map[K, V]

NewMap returns an empty Mutex Map.

func NewMapWithValue added in v0.1.0

func NewMapWithValue[K comparable, V any](m map[K]V) Map[K, V]

NewMapWithValue returns a Mutex Map with the provided map. m is copied into the Mutex Map.

type Numeric

type Numeric[V uint | uint8 | uint16 | uint32 | uint64 | int | int8 | int16 | int32 | int64 | float32 | float64 | complex64 | complex128] interface {
	Value[V]
	// Add adds delta to the value stored.
	Add(delta V) V
}

Numeric is an interface that extends Value with an Add method. The value stored must be a numeric type.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/thetechpanda/mutex"
)

func main() {
	// create a new Value with initial value 0
	m := mutex.NewNumeric[int]()
	m.Store(0)

	// create a wait group to synchronize goroutines
	var wg sync.WaitGroup

	// the following two goroutine add 1 to the value 1000 times
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 1000; i++ {
			m.Add(1)
		}
	}()

	// the following two goroutine subtract 1 to the value 1000 times
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 1000; i++ {
			m.Add(-1)
		}
	}()

	// wait for goroutines to finish
	wg.Wait()

	v, ok := m.Load()
	fmt.Println("value =", v, ", ok =", ok) // value = 0 , ok = true
}
Output:

func NewNumeric

func NewNumeric[V uint | uint8 | uint16 | uint32 | uint64 | int | int8 | int16 | int32 | int64 | float32 | float64 | complex64 | complex128]() Numeric[V]

NewNumeric returns a new Numeric.

func NewNumericWithValue

func NewNumericWithValue[V uint | uint8 | uint16 | uint32 | uint64 | int | int8 | int16 | int32 | int64 | float32 | float64 | complex64 | complex128](v V) Numeric[V]

NewNumericWithValue returns a new Numeric, set to the specified value.

type Value

type Value[V any] interface {
	// Load returns the value stored, or zero value if no
	// value is present.
	// The ok result indicates whether value was set.
	Load() (v V, ok bool)
	// Store sets the value.
	Store(value V)
	// LoadOrStore returns the existing value if present.
	// Otherwise, it stores and returns the given value.
	// The loaded result is true if the value was loaded, false if stored.
	LoadOrStore(value V) (actual V, loaded bool)
	// Swap swaps the value for a key and returns the previous value if any.
	// The loaded result reports whether the key was present.
	Swap(value V) (previous V, loaded bool)
	// CompareAndSwap swaps the old and new values
	// if the value stored in the map is equal to old.
	//
	// Returns true if the swap was performed.
	//
	// ! this function uses reflect.DeepEqual to compare the values.
	CompareAndSwap(old, new V) bool
	// return true if the value is a zero value (not set)
	IsZero() bool
	// Exclusive executes the function f exclusively, ensuring that no other goroutine is accessing the value.
	//
	// The function f is passed the current value and a boolean indicating whether the value is set.
	// The function f should return the new value to be stored.
	//
	// ! Do not invoke any Value or Numeric functions within 'f' to prevent a deadlock.
	Exclusive(f func(v V, ok bool) V) V
	// Clear removes the value from the store.
	Clear()
}

Value is an interface that represents a thread-safe value store. It provides methods to load, store, and manipulate the stored value. The value can be of any type specified by the type parameter V. Pay attention when using pointer types as modifications to the value directly could lead to concurrency issues.

Example
package main

import (
	"fmt"
	"strconv"
	"sync"

	"github.com/thetechpanda/mutex"
)

func main() {
	// create a new Value with initial value 0
	m := mutex.NewValue[string]()
	m.Store("")

	// create a wait group to synchronize goroutines
	var wg sync.WaitGroup

	// the following two goroutine will store values from 0 to 999
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 1000; i++ {
			m.Store(strconv.Itoa(i))
		}
	}()

	// the following two goroutine will store values from 0 to -999
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 1000; i++ {
			m.Store(strconv.Itoa(-i))
		}
	}()

	// wait for goroutines to finish
	wg.Wait()

	// value is either "-999" or "999"
	v, ok := m.Load()
	fmt.Println("value =", v, ", ok =", ok) // "value = -999 , ok = true" or "value = 999 , ok = true"
}
Output:

func NewValue

func NewValue[V any]() Value[V]

NewValue returns a new Value.

func NewWithValue

func NewWithValue[V any](v V) Value[V]

NewWithValue returns a new Value, set to the specified value.

Directories

Path Synopsis

Jump to

Keyboard shortcuts

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