Question about WWDC Video - Protect Mutable State with Swift Actors

In the WWDC talk, “Protect Mutable State with Swift Actors”, the speaker gives an example of a data race and states: “This is expected, and in both cases, the counter would be left in a consistent state.“ What does the speaker mean by the term consistent state, and why is the counter in a consistent state in this case?

Also, I was trying to run the code sample from the slide in the video in my own Playground, and I got this error: “Non-sendable type 'Counter' of let 'counter' cannot exit main actor-isolated context“. How can I fix this?

:waving_hand: Hello @asaadjaber,

:globe_showing_americas: In that WWDC talk, “consistent state” just means that the final value (and the printed results) could have come from some serial execution of the two increment() calls.

For this Counter:

  • If the two tasks print 1 and 2 (in any order) and the final value is 2, that matches some non-concurrent ordering of two increments → the state is consistent.
  • If you ever see 1 & 1 or 2 & 2, those results are impossible for this increment implementation in any purely serial run → that’s an inconsistent state caused by a data race.

:playground_slide: About the Playground error:

Non-sendable type 'Counter' of let 'counter' cannot exit main actor-isolated context

Top-level code runs on the main actor, and Task.detached uses an @Sendable closure, so capturing a non-Sendable class instance across that boundary is rejected. The “fix” in modern Swift is to either make it an actor (safe), or explicitly opt out of safety if you only want to demo a race.

:technologist: A safe version using an actor looks like this:

actor Counter {
    var value = 0

    func increment() -> Int {
        value += 1
        return value
    }
}

let counter = Counter()

Task.detached {
    print(await counter.increment())
}

Task.detached {
    print(await counter.increment())
}

:grinning_face:
Jiaxu Li
Member, Swift C++ Interop Workgroup
Swift Team

1 Like

Thanks for your answer @JiaxuLi ! Follow-up question: how can the two tasks print 2 and 1 in this order if the value needs to be 1 before it can be 2?

Additionally, I am trying to demo a race, so how can I explicitly opt out of safety in this case?

Thank you

1 Like

:waving_hand: Hello @asaadjaber,

Good questions!

how can the two tasks print 2 and 1 in this order if the value needs to be 1 before it can be 2?

Because print order ≠ execution order. One possible interleaving:

  1. Task A: value 0 → 1, returns 1 (but its print is delayed)
  2. Task B: value 1 → 2, returns 2 and prints 2
  3. Later Task A’s print happens and prints 1

So the console shows 2 then 1, but the logical order of increments is still A then B, and the final value is 2. That’s still a “consistent” state.

I am trying to demo a race, so how can I explicitly opt out of safety?

Mark the class as @unchecked Sendable so the compiler lets you do it:

final class Counter: @unchecked Sendable {
    var value = 0

    func increment() -> Int {
        value += 1
        return value
    }
}

let counter = Counter()

Task.detached {
    print(counter.increment())
}

Task.detached {
    print(counter.increment())
}

:grinning_face:
Jiaxu Li
Member, Swift C++ Interop Workgroup
Swift Team

1 Like

Thanks! @JiaxuLi :smiley:

1 Like