Early resumption is the whole point of making sleep(nanoseconds:) throwing, so I'll see about making that abundantly clear in the documentation.
Regarding throwing suspend(), I don't think it makes sense: with sleep, we have early resumption when the task gets cancelled, and I think it's important to distinguish between "at least that much time has passed" and "we finished early because the task was cancelled." suspend() isn't like that: it's just giving other code an opportunity to run on the thread. There's nothing more that needs to be reported.
I don't think we should add the variant that always sleeps for at least the duration, for a few reasons:
- I doubt it's something that's going to be very common, and it's easy to misuse to create unexpected delays in your async tasks. Please provide use cases if you think it's going to be a useful
- It means introducing two APIs with similar names. Then we'd have to go back and do it all again when we get a decent duration type, because "nanoseconds expressed as UInt64" is very much not a good long-term API.
- You can trivially build the "always sleeps for the duration" by creating another task and not cancelling it, e.g.,
func sleepWithoutCancellation(nanoseconds duration: UInt64) {
let task = Task.detached {
try! await Task.sleep(nanoseconds: duration)
}
await task.value
}
Personally, I'm motivated by the ability to create an API that lets you perform a potentially-long-running task, but have an alternative result if the task doesn't complete in some specified duration. That's something one cannot build at all unless you have a sleep that returns early on cancellation and tells you that's what it did. For example:
let image = try await withLongRunningTask(timeout: oneSecond) {
try await downloadImage(url: URL)
} onTimeout: {
timeoutPlaceholderImage
}
As an aside, while thinking about the implementation of this, I realized that we're missing UnsafeCurrentTask.cancel(). We should probably add that.
So, I think we have to report whether the sleep operation was completed normally vs. having returned early because the task was cancelled. Throwing requires one to acknowledge the cancellation. We could instead return a Bool to indicate that the sleep completed successfully, which is slightly more lightweight for the cases where one doesn't care how the sleep ended:
_ = Task.sleep(nanoseconds: duration)
If we like the Bool result and we really, really think it's okay to silently return from sleep before the time has elapsed, we could use @discardableResult.
This is even more subtle than a @discardableResult Bool. It's also somewhat racey, because the sleep might have completed and then the task got cancelled afterword. I don't know if this race matters in practice or not, but it seems poor form not to have sleep report how it ended.
This is more of an implementation detail, but sure.
Doug