Mutex vs actor

Concurrency Senior Swift 6.0+ iOS 18+ Reviewed By Alok Singh

Pick the fix

A counter is incremented from a synchronous C callback on many threads and must stay synchronous. Which tool?

Options
Hint 1 of 2

Can the caller await?

Hint 2 of 2

Which option actually protects the variable?

Choose an answer, then open Show answer below to check it.

Show answer

Answer: B Mutex<Int> from Synchronization with withLock { $0 += 1 }

Correct. A lock keeps the call synchronous and safe (OSAllocatedUnfairLock on iOS 16–17).

Why not the others?
  • A An actor. Callers would have to await, which a synchronous callback can't do.
  • C DispatchQueue.main.sync { count += 1 }. It serializes work onto the UI thread and deadlocks if called on main.
  • D A plain var in a class marked @unchecked Sendable. Nothing protects the variable, so concurrent increments lose updates.

30-second answer

Use an actor when callers can await and the state is accessed from async code. Use Mutex (Synchronization, iOS 18+) or OSAllocatedUnfairLock (iOS 16+) when callers must stay synchronous, the critical section is tiny, and you never await while holding the lock. Both make a type safely Sendable.

Practise saying it · 30 s. No recording, just a timer

Explain when you pick Mutex over an actor.

Say your answer out loud, then check which of these points you covered:

  • Synchronous callers need a lock
  • Tiny critical sections, never await inside
  • Actor when callers can await

Deep dive

Mutex is a noncopyable struct; store it in a let and use withLock { }.

Never await inside a lock; locks and suspension don't mix.

Actors are reentrant; a mutex isn't. Keep critical sections short to avoid contention.

A Sendable final class with a private Mutex is a common pattern for caches and counters.

Sendable counter Type-checked · Swift 6.4
import Synchronization

final class HitCounter: Sendable {
    private let count = Mutex(0)
    func hit() { count.withLock { $0 += 1 } }
    var value: Int { count.withLock { $0 } }
}

Common mistakes

  • Using DispatchQueue.sync as a lock from the main thread (deadlock risk).
  • Holding a lock across slow work.
  • Using an actor for a counter read from synchronous C callbacks.

Senior take & follow-ups

Seniors justify the choice by call-site constraints (sync vs async), not by habit.

Follow-up questions

Think of your answer first, then open the hint.

  1. Why is Mutex noncopyable?

    Show hint

    Copying a lock would split its state; ~Copyable prevents that.

  2. What is priority inversion?

    Show hint

    A low-priority thread holds a lock a high-priority one needs; unfair locks handle it better.

Suggest a fix if something here is wrong or out of date.