Mutex - Thread-Safe Shared State¶
A Mutex (mutual exclusion) protects shared data from concurrent access. When multiple tasks need to read or write the same value, a Mutex ensures only one task can access it at a time.
Creating a Mutex¶
Use the sync module to create a Mutex protecting any value:
import sync
# Protect an integer counter
let counter = sync.Mutex(0)
# Protect a string
let name = sync.Mutex("unknown")
# Protect a custom struct
struct Config:
host: str
port: int
let config = sync.Mutex(Config(host="localhost", port=8080))
The type is inferred automatically: sync.Mutex(42) creates Mutex<int>.
Alternative Syntax
The builtin mutex_new(value) function also works and is equivalent to sync.Mutex(value).
Locking and Accessing Values¶
To access the protected value, you must lock the mutex first:
import sync
let counter = sync.Mutex(0)
# Lock the mutex to get a guard
let guard = counter.lock()
# Access the value through the guard
let current = guard.value
print(current) # 0
The Guard Pattern¶
The .lock() method returns a MutexGuard<T>, not the value directly. This ensures:
- The lock is held while you access the value
- You can't accidentally access the value without locking
import sync
let counter = sync.Mutex(100)
let guard = counter.lock()
# guard.value is the protected int
print(guard.value) # 100
# When guard goes out of scope, the lock is released
Non-Blocking Lock with try_lock()¶
If you don't want to wait for the lock:
import sync
let m = sync.Mutex(42)
let status = match m.try_lock():
Option.Some(guard): "Got lock: " + str(guard.value)
Option.Nothing: "Lock is held by another task"
print(status)
Complete Examples¶
Protecting a Counter¶
import sync
def main() -> int:
let counter = sync.Mutex(0)
# Lock to read
let guard = counter.lock()
print(guard.value) # 0
return 0
Protecting a Configuration¶
import sync
struct ServerConfig:
host: str
port: int
max_connections: int
def main() -> int:
let config = sync.Mutex(ServerConfig(
host="0.0.0.0",
port=8080,
max_connections=100
))
let guard = config.lock()
print("Server on port: " + str(guard.value.port))
return 0
Protecting a Class Instance¶
import sync
class Logger:
pub mut entries: list<str>
pub def __new__(self):
self.entries = []
pub def count(self) -> int:
return len(self.entries)
def main() -> int:
let logger = Logger()
let shared_logger = sync.Mutex(logger)
let guard = shared_logger.lock()
print("Log entries: " + str(guard.value.count()))
return 0
Best Practices¶
1. Keep Critical Sections Short¶
Hold the lock for the minimum time necessary:
import sync
# Good: Short critical section
let guard = counter.lock()
let value = guard.value
# guard is released
# Process value without holding lock
let result = expensive_computation(value)
2. Use try_lock() to Avoid Deadlocks¶
When you can't afford to wait:
# Each arm is a single expression, so call a function to do more than one thing.
let handled = match resource.try_lock():
Option.Some(guard): use_resource(guard.value)
Option.Nothing: fallback_behavior()
3. Import the sync Module¶
Use import sync for clear, modern syntax:
Supported Types¶
Mutex works with all Desi types:
| Type | Example |
|---|---|
int |
sync.Mutex(42) |
float |
sync.Mutex(3.14) |
bool |
sync.Mutex(true) |
str |
sync.Mutex("hello") |
struct |
sync.Mutex(Point(x=1, y=2)) |
class |
sync.Mutex(Counter(0)) |
list |
sync.Mutex([1, 2, 3]) |
When to Use Mutex¶
Use Mutex when:
- Multiple tasks need to read/write the same data
- You need to protect invariants across multiple fields
- Order of access matters (first-come, first-served)
Consider alternatives:
- Channel: When passing data between tasks (message passing)
- Atomic: For simple counters (when available)
See Also¶
- Channels - Message passing between tasks
- TaskGroup - Structured concurrency
- Thread Safety - Concurrency overview