-
-
Save swhitty/9be89dfe97dbb55c6ef0f916273bbb97 to your computer and use it in GitHub Desktop.
extension Task where Failure == Error { | |
// Start a new Task with a timeout. If the timeout expires before the operation is | |
// completed then the task is cancelled and an error is thrown. | |
init(priority: TaskPriority? = nil, timeout: TimeInterval, operation: @escaping @Sendable () async throws -> Success) { | |
self = Task(priority: priority) { | |
try await withThrowingTaskGroup(of: Success.self) { group -> Success in | |
group.addTask(operation: operation) | |
group.addTask { | |
try await _Concurrency.Task.sleep(nanoseconds: UInt64(timeout * 1_000_000_000)) | |
throw TimeoutError() | |
} | |
guard let success = try await group.next() else { | |
throw _Concurrency.CancellationError() | |
} | |
group.cancelAll() | |
return success | |
} | |
} | |
} | |
} | |
private struct TimeoutError: LocalizedError { | |
var errorDescription: String? = "Task timed out before completion" | |
} |
It can be confusing because Swift's concurrency design is intentionally vague about the details of how code is actually run and does not have a construct for threads and instead uses high-level semantic properties, expressed in terms of actor isolation.
When actor isolation is not specified, tasks are performed on the default concurrent executor. John McCall's commentary on its relationship with threads is enlightening
The default concurrent executor is used to run jobs that don't need to run somewhere more specific. It is based on a fixed-width thread pool that scales to the number of available cores.
Apple documentation is shit as usual. Thank you @swhitty & @MrDzmitry for the great discussion! It was a great learning resource for me on async/await.
A Swift 6 compatible version is available within the micro package swhitty/swift-timeout
You're right, but it's not a problem. Each tool has rules how to use it. This one isn't exception. This tool meant to run short timestamps as 5 seconds or some minutes (but I use it for 5 seconds). There is no problem if task, that just sleeps, will stay alive for 5 extra seconds.
imho you're not right here, Task.detached logically is the same as DispatchQueue.global().async. If you never use it you wirte single thread code, that don't use whole cpu power. In my application I use Task.detached in 90% of code and in result I have app, that uses main thread only in 65% cases (I measured it). You need use Task.detached at least for root task. Use child tasks or not it's up to your architecture. In my case I have no dependent tasks, I just don't need them.
It will be truth only if you run tasks that never ends. That is why I said your code is cool. In case if you have tasks that never stops but can easily support cancellation your code is better than mine. But in opposite situation it's visa versa :)
fifty fifty. It might be bug and might be logic. In my case it's business logic.
it can be a good idea to give exact name as withIgnoringCooperativeCancellation, I really though about it, but it's not so easy to do and not so cool looks in code that I decided not to use naming like this. As an example I used this
that never explains this trick about TaskGroup. It was really hard to find that TaskGroup will wait till each task ends. I even found that forum discussion where apple developer didn't know about this behavior in first time. So...
I'm not insists, if you like use only one thread, instead of timeout wait till work cancels and so on - you are on a right way. But I have need to finish work after UI passed this part (cause I will reuse results in future) and prefer to use all threads that I can.