With the work-in-progress backport of the new Swift concurrency model all the way to iOS13, async/await and friends are getting more and more relevant for all Swift developers.

So here’s a quick and simple example that showcases some of the nice features of the new concurrency model without going into much detail.

Thread.sleep() vs Task.sleep()

Let’s just look at the old and new sleep APIs:

  • Thread.sleep() is the old API that blocks a thread for the given amount of seconds โ€” it doesn’t load the CPU, the core just idles. Edit: Turns out the core doesn’t idle but DispatchQueue.concurrentPerform creates only as many threads as there are cores so even if the blocked treads don’t do anything it doesn’t create more threads to do more work during this time.

  • Task.sleep() is the new async API that does “sleep”. It suspends the current Task instead of blocking the thread โ€” that means the CPU core is free to do something else for the duration of the sleep.

Or to put them side by side:

Thread.sleep() Task.sleep()
Blocks the thread Suspends and lets other tasks run
Slow switching of threads on same core Quickly switching via function calls to a continuation
Rigid, cannot be cancelled Can be cancelled while suspended
Code resumes on the same thread Could resume on another thread, threads don’t matter

The difference in practice

What do these mean in practice? This is how you’d concurrenty have 100 sleeps with DispatchQueue:

1
2
3
4
5
6
let start = Date.timeIntervalSinceReferenceDate
DispatchQueue.concurrentPerform(iterations: 100) { _ in
  Thread.sleep(forTimeInterval: 1)
}
let end = Date.timeIntervalSinceReferenceDate
print(String(format: "Duration: %.2fs", end-start))

GCD spreads the concurrent work on all cores it can grab and on my machine the code prints:

Duration: 9.00s

The thing is โ€” you’re limited to the amount of threads DispatchQueue will create for you as switching threads is very expensive and the system will try creating as few as possible. In the end the machine is “busy” running nothing.

Let’s have a look at the same code with the new Task APIs and async/await:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let start = Date.timeIntervalSinceReferenceDate
await withThrowingTaskGroup(of: Void.self, body: {
  for _ in 0 ..< 100 {
    $0.addTask {
      try await Task.sleep(nanoseconds: 1_000_000_000)
    }
  }
})
let end = Date.timeIntervalSinceReferenceDate
print(String(format: "Duration: %.2fs", end-start))

Running this code prints:

Duration: 1.05s

While suspended, a Task takes no CPU core-seconds so the same core can run all of the hundred sleep tasks at the same time. You can have thousands of them if you want and the duration will still be just over 1 second.

In the new model, asynchronous tasks collaborate by suspending when not actively doing work so all of the currently scheduled work can progress forward.

Thanks for reading and see you next time ๐Ÿ‘‹๐Ÿฝ

Where to go from here?

Interested in the new async/await Swift syntax? Hit me up on twitter at https://twitter.com/icanzilb.