Skip to content

swift-cloud/swift-concurrent-dictionary

Repository files navigation

ConcurrentDictionary

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 async functions and task groups without await or 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
  • Sendable by 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 Sendable conformance 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 1

Swift 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

  1. Avoid frequent count/isEmpty checks - These acquire all stripe locks sequentially
  2. Choose appropriate stripe count - Too few causes contention, too many wastes memory
  3. Use getOrSetValue for caches - Provides atomic get-or-insert semantics
  4. Use incrementValue for counters - Atomic increment without race conditions
  5. Prefer over actor-wrapped dictionaries - When you need shared mutable state accessed from multiple isolation domains, ConcurrentDictionary avoids actor hop overhead
  6. Use with Sendable types - Both keys and values must be Sendable for compile-time thread safety

License

MIT License

About

High performance concurrent Dictionary for Swift

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages