A high-performance, thread-safe dictionary for Swift using striped locking for minimal contention.
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
- 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
- Swift 6.2+
- macOS 26.0+ / iOS 26.0+ / tvOS 26.0+ / watchOS 26.0+
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")
]
)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>()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)")
}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")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")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 1The 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)")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)// 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()
}
}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 |
| 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) |
- 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
MIT License