Irbisa · Cheatsheet

Swift Concurrency

async/await, Task, actors, structured concurrency, AsyncSequence

Middle Developer · 25 questions

  1. 01

    What is the difference between concurrency and parallelism, and between synchronous and asynchronous execution?

    Concurrency means several tasks are in progress at the same time and may interleave; parallelism means they literally execute at the same instant on different CPU cores.

  2. 02

    What is GCD, and what is the difference between serial and concurrent queues and between sync and async dispatch?

    GCD (Grand Central Dispatch) is Apple's library for submitting closures to queues that the system runs on a thread pool it manages.

    let serial = DispatchQueue(label: "com.app.cache")   // serial by defaultlet concurrent = DispatchQueue(label: "com.app.io", attributes: .concurrent) serial.async { print("1") }    // returns immediatelyserial.async { print("2") }    // always runs after "1"…
  3. 03

    How does async/await work in Swift, and how does it differ from completion handlers?

    An async function can suspend at each await and resume later, and while suspended it holds no thread.

    // Before: completion handlerfunc loadUser(id: String, completion: @escaping (Result<User, Error>) -> Void) {  // ...} // After: async/await…
  4. 04

    How do you call an async function from synchronous code, and can you call synchronous code from an async function?

    From synchronous code you start a Task { } and await inside it; the synchronous function returns immediately and never sees the result directly.

    @MainActor@Observablefinal class ProfileViewModel {  var profile: Profile?  private var loadTask: Task<Void, Never>?…
  5. 05

    What does this code print when test() runs on the main thread, and what changes if it runs on a background queue?

    func test() {  print("A")  DispatchQueue.main.async { print("B") }  print("C")  DispatchQueue.main.sync { print("D") }  print("E")}test()   // called on the main thread

    On the main thread it prints A, C and then gets stuck: main.sync waits for the main queue to run D, but the main queue is busy running test() itself, which is waiting; this is a deadlock that libdispatch usually turns into a crash.

  6. 06

    What are the Quality of Service classes, and how do they affect scheduling?

    QoS tells the system how much the work matters to the user right now, and the system uses it to set thread priority, share CPU time and choose between performance and efficiency cores.

  7. 07

    What is DispatchGroup, and when do you use it?

    DispatchGroup counts a set of asynchronous jobs and tells you when all of them have finished; the classic case is firing several requests in parallel and updating the UI once every one has returned.

    func loadAll(ids: [String], completion: @escaping ([User]) -> Void) {  let group = DispatchGroup()  let lock = NSLock()  var users: [User] = []   for id in ids {…
  8. 08

    What is the difference between GCD and OperationQueue, and when would you pick OperationQueue?

    OperationQueue is an Objective-C layer over GCD that wraps work in Operation objects and adds dependencies, a concurrency limit, cancellation and observable state; GCD just runs closures.

  9. 09

    What is the difference between a race condition, a data race and a deadlock, and how do you prevent each?

    A data race is two threads touching the same memory at once, at least one writing, with no synchronisation: undefined behaviour.

  10. 10

    What is a semaphore, how does it differ from a mutex, and when is DispatchSemaphore a bad idea?

    A semaphore is a counter that lets up to N threads through and blocks the rest; a mutex lets exactly one thread into a critical section and has an owner: only the thread that locked it may unlock it.

    import Synchronization // Mutex: one thread at a time, and the owner unlocksfinal class Counter: Sendable {  private let value = Mutex(0)  func increment() { value.withLock { $0 += 1 } }…
  11. 11

    What is structured concurrency, and when do you use async let versus a task group?

    Structured concurrency binds child tasks to the scope that created them: they cannot outlive it, cancellation flows down, errors flow up, and the parent waits for every child.

    // Fixed fan-out: three different requests in parallelfunc loadDashboard() async throws -> Dashboard {  async let user = api.user()  async let posts = api.posts()   // all three start right here  async let news = api.news()  return try await Dashboard(user: user, posts: posts, news: news)…
  12. 12

    What is the difference between Task { } and Task.detached, and how is a task’s priority decided?

    Task { } inherits the actor isolation, priority and task-local values of the code that creates it; Task.detached inherits none of them and runs nonisolated with its own priority.

    @MainActor@Observablefinal class FeedModel {  var items: [Item] = []   func refresh() {…
  13. 13

    How does task cancellation work in Swift Concurrency?

    Cancellation is cooperative: cancel() only sets a flag on the task and all its children, and nothing stops until code checks that flag or awaits an API that checks it for you.

    func processAll(_ items: [Item]) async throws {  for item in items {    try Task.checkCancellation()        // stop between units of work    try await process(item)  }}…
  14. 14

    Find the bug: after the screen calls stop(), the ticker keeps hitting the API and CPU usage jumps.

    // api: a Sendable global whose fetchPrice() is async throws -> Decimal@MainActorfinal class PriceTicker {  private var task: Task<Void, Never>?  private(set) var price: Decimal = 0   func start() {    task = Task {      while true {        if let p = try? await api.fetchPrice() { price = p }        try? await Task.sleep(for: .seconds(5))      }    }  }  func stop() { task?.cancel() }}

    try? swallows the CancellationError that tells the loop to stop.

  15. 15

    loadUser() throws after 0.1 s while loadPosts() takes 10 s: what happens to the posts request, and when does loadScreen() return?

    func loadScreen() async throws -> Screen {  async let posts = api.loadPosts()   // takes 10 s  async let user = api.loadUser()     // throws after 0.1 s  let u = try await user  let p = try await posts  return Screen(user: u, posts: p)}

    The error surfaces at try await user, and before the function exits Swift implicitly cancels posts and waits for it, so loadScreen() throws only once the posts task has wound down: almost immediately if loadPosts honours cancellation, after the full 10 s if it ignores it.

  16. 16

    You need to download 500 images but no more than 5 at a time: how do you cap concurrency in a task group?

    Seed the group with 5 children, then every time one finishes (next() returns), add the next one, so the group never has more than 5 in flight.

    func downloadAll(_ urls: [URL], maxConcurrent: Int = 5) async throws -> [URL: Data] {  try await withThrowingTaskGroup(of: (URL, Data).self) { group in    var pending = urls.makeIterator()    var results: [URL: Data] = [:]     // Seed the first `maxConcurrent` downloads…
  17. 17

    How do you add a timeout to an await call when the API has no timeout parameter?

    Race the operation against Task.sleep in a throwing task group: take whichever child finishes first and cancel the other.

    struct TimeoutError: Error {} func withTimeout<T: Sendable>(  _ duration: Duration,  operation: @escaping @Sendable () async throws -> T) async throws -> T {…
  18. 18

    What is an actor, how does it prevent data races, and why do you await its methods from outside?

    An actor is a reference type that protects its mutable state by running only one piece of its code at a time on its own serial executor.

    actor BankAccount {  let id: UUID  private(set) var balance: Decimal = 0   init(id: UUID) { self.id = id }…
  19. 19

    What does @MainActor guarantee, why not mark everything with it, and how do you move work off it?

    @MainActor is a global actor whose executor is the main thread: annotated code is guaranteed to run there, and the compiler makes callers from other isolation domains await it.

    @MainActor@Observablefinal class FeedViewModel {  var items: [Item] = []   func reload() async throws {…
  20. 20

    What is Sendable, and which types conform to it automatically?

    Sendable is a marker protocol saying a value can cross concurrency domains (between tasks, into or out of an actor) without causing a data race.

    import Synchronization struct User: Sendable {            // all stored properties are Sendable  let id: UUID  var name: String}…
  21. 21

    How do you wrap a completion-handler API in async/await, and what is the difference between checked and unsafe continuations?

    Call withCheckedThrowingContinuation, start the legacy call inside it, and resume the continuation exactly once from the callback; the awaiting task stays suspended until then.

    // Legacy APIfunc fetchUser(id: String, completion: @escaping (Result<User, Error>) -> Void) {  // ...} // Async wrapper…
  22. 22

    What are AsyncSequence and AsyncStream, and when do you use them?

    AsyncSequence is a sequence whose elements arrive over time and is consumed with for await; AsyncStream is the ready-made one you build yourself to turn callbacks, delegates or notifications into such a sequence.

    // LocationTracker: a Sendable wrapper around a delegate-based APIfunc locationUpdates() -> AsyncStream<CLLocation> {  AsyncStream(bufferingPolicy: .bufferingNewest(1)) { continuation in    let tracker = LocationTracker()    tracker.onUpdate = { continuation.yield($0) }    continuation.onTermination = { _ in tracker.stop() }   // cancelled or finished…
  23. 23

    This actor still downloads the same image twice when two views ask for it at once: why, and how do you fix it?

    // downloader: a Sendable global dependency whose fetch(_:) is asyncactor ImageCache {  private var cache: [URL: UIImage] = [:]   func image(for url: URL) async throws -> UIImage {    if let cached = cache[url] { return cached }    let image = try await downloader.fetch(url)   // suspension point    cache[url] = image    return image  }}

    Actors are reentrant: at the await the actor is released, the second call runs, finds the cache still empty and starts its own download.

  24. 24

    What is the cooperative thread pool, and why is blocking it with DispatchSemaphore.wait() or Thread.sleep a bug?

    Swift Concurrency runs tasks on a cooperative pool with about one thread per CPU core and assumes every task either runs or suspends.

    // wrong: called from async code, this blocks a pool thread; one per core and the app is wedgedfunc loadSync() -> Data {  let semaphore = DispatchSemaphore(value: 0)  var result = Data()  Task {    result = (try? await api.download()) ?? Data()…
  25. 25

    How do you protect shared mutable state (actor, lock or serial queue) and how do you choose between them?

    Use an actor when callers are async or the critical section must await; a lock (Mutex, OSAllocatedUnfairLock) when access is short, synchronous and hot; a serial DispatchQueue mainly inside GCD code.

    import Synchronization // Hot, synchronous, tiny critical section: a lockfinal class HitCounter: Sendable {  private let counts = Mutex<[String: Int]>([:])  func record(_ key: String) { counts.withLock { $0[key, default: 0] += 1 } }…