Task.sleep vs Timer, tolerances?

I have an app for timing physical exercises. It uses the Foundation Timer class to get a tick every second. (I'm not sure if that's the best way.) Could Task.sleep(...) be used for this? Is it not as precise? The docs say:

"Suspends the current task for at least the given duration in nanoseconds."

There is no mention of how hard it tries to not go beyond the given duration. The Timer class has a tolerance property.

It uses the Foundation Timer class to get a tick every second.

What platform is this on? And presuming it’s on an iOS-based platform, can you guarantee that your app won’t be suspended while the timer is running?

This matters because of the way that Timer counts time. There’s actually two wrinkles here:

  • If your app gets suspended, Timer won’t resume it.

  • If your app gets suspended then the device might sleep, which affects whether Timer hits the deadline you give it.

Time is Hard™

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

3 Likes

Yes, it's iOS. (Though now that I've ported to SwiftUI I may do a macOS one too.) This is app is about six years old. There may be a more modern way to handle this, but when I first created the app, I used a "background audio" permission and then played imperceptible audio, to keep the app going. It was the only way I knew for "workout" apps to stay alive and timing an exercise. I think in recent versions of iOS, I only need to keep an AVAudioSession alive. I don't actually play a sound the entire time. The app does play audio or speech synthesis at specific intervals.

I used a "background audio" permission and then played imperceptible
audio, to keep the app going.

This isn’t a supported technique.


Regardless, I can answer your original question. You wrote:

There is no mention of how hard it tries to not go beyond the given
duration.

This is limited by the Swift concurrency cooperative thread pool. When a task comes out of sleep, the concurrency runtime has to find a thread to run it on. It pulls this from the afore-mentioned thread pool. If no threads are available, the task has to wait. The runtime will not start a new thread (no overcommit), nor can it preempt an existing one (hence the cooperative).

So, the amount of delay is only bounded by how long your async/await code runs between await calls [1].

WWDC 2021 Session 10254 Swift concurrency: Behind the scenes talks about this stuff in depth. I highly recommend it.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

[1] This assumes you’re running enough task to use all the threads in the pool. Currently the pool is sized at a thread per core, so it’s relatively simple to do that.

Terms of Service

Privacy Policy

Cookie Policy