swift-cloud/swift-concurrent-dictionary
A high-performance, thread-safe dictionary for Swift using striped locking for minimal contention.
Designed for Swift Concurrency
ConcurrentDictionary is built from the ground up for Swift's modern concurrency model. Unlike standard dictionaries that require manual synchronization with actors or locks, this implementation is fully Sendable and can be safely shared across tasks, actors, and isolation boundaries without additional wrapper code.
Why it's a great fit for Swift concurrency:
- Zero boilerplate - Use directly in
asyncfunctions and task groups withoutawaitor actor isolation - No actor bottlenecks - Striped locking allows true parallel access, avoiding the serialization overhead of actor-based solutions
- Synchronous API - All operations are non-async, eliminating unnecessary suspension points
Sendableby default - Pass freely between isolation domains with compile-time safety guarantees
Features
- Thread-safe read and write access from multiple concurrent tasks
- Striped locking strategy for high throughput under contention
- Compile-time configurable stripe count
- Zero dependencies beyond Swift standard library and XXH3
- Full
Sendableconformance for seamless use across isolation boundaries
Requirements
- Swift 6.2+
- macOS 26.0+ / iOS 26.0+ / tvOS 26.0+ / watchOS 26.0+
Installation
Add the package to your Package.swift:
dependencies: [
.package(url: "https://github.com/swift-cloud/swift-concurrent-dictionary.git", from: "1.0.0")
]Then add the dependency to your target:
.target(
name: "YourTarget",
dependencies: [
.product(name: "ConcurrentDictionary", package: "swift-concurrent-dictionary")
]
)Usage
Creating a Dictionary
The stripe count is specified as a compile-time generic parameter. Choose a value based on your expected concurrency level:
import ConcurrentDictionary
// Create a dictionary with 16 stripes (good for moderate concurrency)
let cache = ConcurrentDictionary<16, String, Data>()
// Create a dictionary with 64 stripes (for high concurrency scenarios)
let highConcurrencyCache = ConcurrentDictionary<64, URL, Response>()Basic Operations
let dict = ConcurrentDictionary<8, String, Int>()
// Set a value
dict["score"] = 100
// Get a value
if let score = dict["score"] {
print("Score: \(score)")
}
// Remove a value
dict["score"] = nil
// Or use removeValue to get the old value
if let removed = dict.removeValue(forKey: "score") {
print("Removed: \(removed)")
}Default Values
let settings = ConcurrentDictionary<8, String, String>()
// Get with a default (does not store the default)
let theme = settings["theme", default: "light"]
// Update existing value or set new one
settings.updateValue("dark", forKey: "theme")Atomic Get-or-Set
Use getOrSetValue when you need atomic get-or-insert semantics:
let cache = ConcurrentDictionary<16, String, ExpensiveObject>()
// If key exists, returns existing value
// If key is absent, inserts and returns the new value
// The entire operation is atomic
let value = cache.getOrSetValue(ExpensiveObject(), forKey: "key")Atomic Increment (Numeric Values)
For numeric values, use incrementValue for atomic counter operations:
let counters = ConcurrentDictionary<8, String, Int>()
// Increment (starts at 0 if key doesn't exist)
counters.incrementValue(forKey: "page_views", by: 1)
counters.incrementValue(forKey: "page_views", by: 1)
// Decrement
counters.incrementValue(forKey: "page_views", by: -1)
// Get the new value
let views = counters.incrementValue(forKey: "api_calls", by: 1) // Returns 1Swift Concurrency Integration
The dictionary is Sendable and designed for direct use with structured concurrency:
let metrics = ConcurrentDictionary<16, String, Int>()
// Use directly in task groups - no actor wrapper needed
await withTaskGroup(of: Void.self) { group in
for i in 0..<1000 {
group.addTask {
// Synchronous access from concurrent tasks
metrics.incrementValue(forKey: "requests", by: 1)
metrics["task-\(i)"] = i
}
}
}
print("Total requests: \(metrics["requests", default: 0])")
print("Total entries: \(metrics.count)")Sharing Across Actors
Unlike regular dictionaries, ConcurrentDictionary can be shared across actor boundaries:
actor MetricsCollector {
let store = ConcurrentDictionary<16, String, Int>()
func record(_ event: String) {
store.incrementValue(forKey: event, by: 1)
}
}
actor RequestHandler {
let metrics: ConcurrentDictionary<16, String, Int>
init(metrics: ConcurrentDictionary<16, String, Int>) {
self.metrics = metrics // Safe to share - it's Sendable
}
func handleRequest() {
metrics.incrementValue(forKey: "requests", by: 1)
}
}
// Share the same dictionary across actors
let sharedMetrics = ConcurrentDictionary<16, String, Int>()
let collector = MetricsCollector()
let handler = RequestHandler(metrics: sharedMetrics)Cache Pattern
// No actor needed - ConcurrentDictionary handles synchronization internally
final class DataService: Sendable {
private let cache = ConcurrentDictionary<32, URL, Data>()
func fetchData(from url: URL) async throws -> Data {
// Synchronous cache check - no await needed
if let cached = cache[url] {
return cached
}
// Fetch and cache
let data = try await URLSession.shared.data(from: url).0
cache[url] = data
return data
}
func clearCache() {
cache.removeAll()
}
}Performance Considerations
Stripe Count
The stripe count determines the level of parallelism:
| Stripe Count | Use Case | |--------------|----------| | 4-8 | Low concurrency, memory constrained | | 16-32 | Moderate concurrency (recommended default) | | 64+ | High concurrency, many concurrent writers |
Operations Complexity
| Operation | Complexity | |-----------|------------| | subscript (get/set) | O(1) average | | removeValue | O(1) average | | updateValue | O(1) average | | getOrSetValue | O(1) average | | incrementValue | O(1) average | | count | O(stripes) | | isEmpty | O(stripes) |
Best Practices
- Avoid frequent
count/isEmptychecks - These acquire all stripe locks sequentially - Choose appropriate stripe count - Too few causes contention, too many wastes memory
- Use
getOrSetValuefor caches - Provides atomic get-or-insert semantics - Use
incrementValuefor counters - Atomic increment without race conditions - Prefer over actor-wrapped dictionaries - When you need shared mutable state accessed from multiple isolation domains,
ConcurrentDictionaryavoids actor hop overhead - Use with
Sendabletypes - Both keys and values must beSendablefor compile-time thread safety
License
MIT License
Package Metadata
Repository: swift-cloud/swift-concurrent-dictionary
Default branch: main
README: README.md