Swift Concurrency and Cryosleep: Lessons from Aliens and Alien 3
4 min read
When a Swift task hits an await, it’s like going into cryosleep at the end of Aliens.
Ripley closes her eyes — and wakes up in Alien 3. The world has changed. Her crew is gone. Everything she thought was true before the sleep is no longer safe to assume.
That’s what can happen at every suspension point in Swift Concurrency.
What’s a Suspension Point?
In Swift, any time you await
something, your task can pause and let others run. When it resumes, it might wake up:
- on a different thread
- after other tasks have mutated shared state
- or in a world where your previous assumptions and invariants no longer hold
In this post, we'll focus on what changes while you're in cryosleep: how the state you left behind can mutate, and why the assumptions you made before closing your eyes may be dangerously wrong when you open them again.
After a suspension point (an await
call), an async function should "recheck its surroundings" as it were. Don’t assume that values, invariants, or even actors are in the same state you left them:
- don’t rely on assumptions across awaits
- recheck important conditions after suspension
- keep critical sections atomic
Treat every await
like being in cryosleep or time travel. When you wake up, ask the equivalents of:
- what time is it?
- where am I?
- what's happened?
- do I still have my stuff?
- has anyone messed with my stuff?
Code Example: Not Checking Assumptions
Let's use the Ripley / Aliens / Alien 3 scenario for some coding examples.
We'll have some code that:
- creates an instance of an actor type
CryoShipBuggy
that is our simulation of the events of the films - Ripley goes into cryosleep
- we wait before simulating the beginning of Alien 3 where Ripley's ship crashes
- we wait long enough for Ripley to wake up from cryosleep and resume
@main
struct Concurrency_Aliens {
static func main() async {
let ship = CryoShipBuggy()
// Start Ripley's cryosleep (suspends)
Task {
await ship.ripleyCryosleep()
}
// Give Ripley a moment to enter cryosleep
try? await Task.sleep(for: .milliseconds(500))
// Shuttle crash occurs while she is suspended
await ship.shuttleCrash()
// Wait long enough for Ripley to wake up
try? await Task.sleep(for: .seconds(3))
}
}
So lets take a look at the buggy implementation:
actor CryoShipBuggy {
var crew: [String] = ["Newt", "Hicks"]
var location: String = "Cryo-Shuttle"
// Ripley enters cryosleep expecting a happy life on Earth with her crew
func ripleyCryosleep() async {
print("😴 Ripley goes into cryosleep, dreaming of a happy life on Earth with: \(crew)")
// Suspension point: hypersleep
try? await Task.sleep(for: .seconds(2))
// Bug! We don't check assumptions after waking...
print("🛡️ Ripley starts her happy life on \(location) with: \(crew)")
}
// Alien 3 shuttle crash occurs while Ripley is suspended
func shuttleCrash() {
print("💥 Shuttle crash! Ripley wakes up alone on Fiorina 161...")
crew.removeAll()
location = "Fiorina 161"
}
}
Note how in the code after the suspension point of hypersleep, we make the following assumption:
- that location will be a place where Ripley can start a happy life
- that her crew are in the same state as before she went to sleep (suspension point)
Of course our code simulates the events at the movies, and so our actual output is:
😴 Ripley goes into cryosleep, dreaming of a happy life on Earth with: ["Newt", "Hicks"]
💥 Shuttle crash! Ripley wakes up alone on Fiorina 161...
🛡️ Ripley starts her happy life on Fiorina 161 with: []
Needless to say Ripley is not having a happy life on Fiorina 161 (the planet Alien 3 is set on), without any surving crew.
This is exactly how suspension points can surprise you in Swift Concurrency. The state may have changed while your task was suspended — actor isolation prevents data races, but reentrancy lets other actor methods run, just like Ripley woke up in a changed world.
Let's try again, but this time taking into account the world may change after a suspension point.
Code Example: Check Your World After You Wake
Ok, we'll do another version of the simulation, where the code performs a post-suspension check on the state of the world (and its assumptions):
actor CryoShipCheckStateOfUniverse {
var crew: [String] = ["Newt", "Hicks"]
var location: String = "Cryo-Shuttle"
// Ripley enters cryosleep expecting a happy life on Earth with her crew
func ripleyCryosleep() async {
print("😴 Ripley goes into cryosleep, dreaming of a happy life on Earth with: \(crew)")
// Suspension point: hypersleep
try? await Task.sleep(for: .seconds(2))
// Check assumptions (or invariants) after waking...
let assumptionsHold = crew.contains("Newt") && crew.contains("Hicks") && location == "Earth"
if assumptionsHold {
print("🛡️ Ripley starts her happy life on \(location) with: \(crew)")
} else {
print("⚠️ Wake-up shock! Assumptions invalid.")
print("Current crew: \(crew), Location: \(location)")
// Take alternative action
adaptAndSurvive()
}
}
// Alien 3 shuttle crash occurs while Ripley is suspended
func shuttleCrash() {
print("💥 Shuttle crash! Ripley wakes up alone on Fiorina 161...")
crew.removeAll()
location = "Fiorina 161"
}
func adaptAndSurvive() {
let crewDescription = crew.count > 0 ? "with " + crew.joined(separator: ",") : "alone"
print("🛠️ Ripley adapts: moves to survive on \(location) \(crewDescription).")
}
}
And here is the output from this code:
😴 Ripley goes into cryosleep, dreaming of a happy life on Earth with: ["Newt", "Hicks"]
💥 Shuttle crash! Ripley wakes up alone on Fiorina 161...
⚠️ Wake-up shock! Assumptions invalid.
Current crew: [], Location: Fiorina 161
🛠️ Ripley adapts: moves to survive on Fiorina 161 alone.
Key Point
Suspension points don’t isolate you from change:
- always recheck your invariants after an await
- actor reentrancy and other tasks can modify shared state while you’re suspended
Or put another way:
Think of any await
in Swift being like Ripley going into cryosleep. Your whole world might have changed by the time you awake. Act accordingly.
Updates
v1.1
In the "What’s a Suspension Point?" section I changed "on a different thread or executor" to "on a different thread".
Thanks to Matt Massicotte for spotting this and providing the feedback. You should definitely check out his blog for more Swift Concurrency stuff.