Concurrency Deep Dive

Actors vs Locks: When Concurrency Changes Everything

January 19, 2026
5 min read
Featured image for blog post: Actors vs Locks: When Concurrency Changes Everything

The conventional wisdom says locks are faster than actors. Benchmarks tell a different story—but only under concurrency.

Follow along with the code: iOS-Practice on GitHub

The Big Insight

Actors win under concurrency. Locks win in serial code.

This isn't intuition—it's measurable. At 50,000 concurrent operations, Actor completed in 62ms while the fastest lock took 167ms. Actor was 2.8x faster.

But flip to serial execution: os_unfair_lock finished in 10.7ms while Actor took 416ms. Locks were 39x faster.

Understanding why changes how you choose synchronization primitives.

The Numbers

ScenarioActorFastest LockWinner
Balanced concurrent61.9 ms167.3 msActor 2.8x
Read Heavy concurrent64.3 ms168.7 msActor 2.7x
Write Heavy concurrent62.7 ms162.7 msActor 2.7x
Heavy Work concurrent677 ms4,874 msActor 7.5x
Serial (no concurrency)416 ms10.7 msLock 39x

Why Actors Dominate Concurrency

When GCD dispatches 50,000 operations to a concurrent queue, it creates threads—potentially hundreds. They all compete for the same lock:

let concurrentQueue = DispatchQueue(label: "test", attributes: .concurrent)
for i in 0..<50_000 {
    concurrentQueue.async {
        self.lock.lock()
        defer { self.lock.unlock() }
        self.values.append(i)
    }
}

Result: threads spinning, context-switching, burning CPU on contention instead of work.

Swift's actor runtime uses cooperative scheduling. It limits thread count, suspends tasks efficiently at await points, and resumes them without kernel-level context switches:

await withTaskGroup(of: Void.self) { group in
    for i in 0..<50_000 {
        group.addTask {
            await self.actor.write(i)
        }
    }
}

The actor processes requests serially, but the system doesn't waste resources fighting over a lock.

Why Locks Dominate Serial Code

A lock operation is a few assembly instructions:

for i in 0..<50_000 {
    lock.lock()
    values.append(i)
    lock.unlock()
}

An actor call requires:

  1. Task suspension
  2. Executor scheduling
  3. Potential context switch
  4. Task resumption

In serial code, that overhead is pure cost with no benefit—hence the 39x difference.

The Decision Framework

SituationChoiceWhy
New concurrent codeActor2.5-3x faster + compile-time safety
Heavy work in critical sectionActor7.5x faster, less thread contention
Obj-C interopNSLockActors don't bridge
iOS 16+ lock-basedOSAllocatedUnfairLockModern API, same performance
Hot serial code pathLocks39x faster, no async overhead
Mixed/unknown workloadActorSafety wins, good enough perf

The Actor Pattern

When you choose actors, the code is clean:

actor ContainerActor {
    private var values: [Int] = []

    func write(_ value: Int) {
        values.append(value)
    }

    func read() -> Int {
        values.last ?? 0
    }
}

No locks to manage. No deadlock risk. No forgotten unlocks. The compiler enforces isolation.

When to Reach for Locks

Locks still have their place:

  1. Hot serial paths - Profiling shows async overhead matters (39x difference!)
  2. Obj-C interoperability - Actors don't bridge to Objective-C
  3. Synchronous requirements - When you can't use async/await
  4. Existing lock-based code - Don't refactor without measuring

Interview Tip

"I default to actors for new Swift code—they're safe by design and 2.5-3x faster under concurrency. But I know locks are 39x faster in serial code, so I profile before making that tradeoff. The heavy work scenario is interesting—actors are 7.5x faster there because long lock hold times cause massive thread contention."

Originally published on deadpixel.llc

© 2026 Christopher Moore / Dead Pixel Studio

Let's work together

Professional discovery, design, and complete technical coverage for your ideas

Get in touch