`Task.sleep` vs. `Task.sleep(for: tolerance: clock:)` as replacement for `Timer`

I'd like to avoid using Timer in some Swift concurrency code; apart from looking odd in the context of that code, it also looks like Runloop might violate some Swift 6 rules (the documentation says not to access the Runloop off of the thread that owns it, but main is not marked @MainActor).

The documentation of Task.sleep() says that that it suspends for "at least" the given duration. Task.sleep(for: tolerance: clock:), however, says that it suspends "until the given deadline within a tolerance". It seems that the latter is used in AsyncAlgorithms for AsyncTimerSequence.

I have two questions. The first is whether Task.sleep(for: tolernance: clock:) is an appropriate substitute for Timer? The second: what the difference is between these two functions that allows one to have a stronger guarantee than the other? Or is this just an oversight or typo in the documentation?

Why typo? AFAIK, the non tolerance version will wake up at some point after duration, while the tolerance version will wake up at some point within [duration - tolerance ... duration + tolerance] interval

BTW, sleep(until:) (optionally with tolerance) looks better to mimic Timer's drift free behaviour.

There's no real difference in the documentation beyond a kind of word-choice preference. You could remove "at least" from the first function's documentation, and you could say "suspends at least until the given deadline within a tolerance" in the second's, without changing the intended meaning of either.

In both cases, the documentation (correctly) does not specify what happens to execution after the specified sleep is over. There is no API contract that, at expiration of the relevant time, execution resumes right away. It may or may not.

So, sleeping for 5 seconds and sleeping for at least 5 seconds are the same thing as each other. Sleeping until 9pm and sleeping until at least 9pm are also the same thing as each other. At least, they're the same in terms of suspension.

I don't have any direct knowledge, but I think it's safe to assume that the two functions are internally identical in their approach to tolerances. The only difference is that one defaults to a certain tolerance, and the other allows you to specify a tolerance, which happens to be optional.

Incidentally, Timer in its ur-life as NSTimer has also had tolerances for perhaps 20 years or so. It's had an API-level tolerance for a shorter period of time, but IIRC NSTimer objects were subject to coalescing at the system level (around, e.g., system sleep/wake behavior) for a few years before that.

Therefore, use whichever Task.sleep you prefer.

2 Likes