I have a proposal to include cancellation support with the upcoming async/await feature, which I wrote it up in a fork off of Chris Lattner's and Joe Groff's "Async/Await for Swift" gist:
https://gist.github.com/dougzilla32/ce47a72067f9344742e10020ad4c8c41
https://gist.github.com/dougzilla32/ce47a72067f9344742e10020ad4c8c41/revisions
The motivation is to provide a consistent and simple API to cancel asynchronous tasks. My theory (seemingly shared by some) is that supporting cancellation is too much trouble for many programmers, because it is a bit involved and the app may still basically work without it. But performance will likely suffer. With this proposal cancellation becomes a simple task so is much more likely to be implemented by app developers.
I created a prototype to see if the proposal actually works (it does!), which simulates async/await and cancellation: https://github.com/dougzilla32/AsyncCancellation
The idea is to have a flavor of beginAsync that returns something that can be cancelled, which would in turn cancel all enclosed asynchronous operations:
/// Defines an asynchonous task for the purpose of cancellation.
protocol AsyncTask {
func cancel()
var isCancelled: Bool { get }
}
/// A list of async tasks along with the associated error handler
public protocol AsyncTaskList: AsyncTask {
var tasks: [(task: AsyncTask, error: (Error) -> ())] { get }
}
/// Returns an 'AsyncTaskList' that can be used to cancel the enclosed
/// asynchronous tasks.
func beginAsyncTask(_ body: () async throws -> Void) rethrows -> AsyncTaskList
/// Invoking 'task' will add the given 'AsyncTask' to the chain of tasks enclosed by
/// 'beginAsyncTask', or does nothing if enclosed by 'beginAsync'.
func suspendAsync<T>(
_ body: (_ continuation: @escaping (T) -> (),
_ error: @escaping (Error) -> (),
_ task: @esccaping (AsyncTask) -> ()) -> ()
) async throws -> T
It is possible to extend AsyncTaskList to add other methods:
/// Add 'suspend' and 'resume' capabilities to AsyncTaskList
extension AsyncTaskList {
func suspend() { tasks.forEach { ($0.task as? URLSessionTask)?.suspend() } }
func resume() { tasks.forEach { ($0.task as? URLSessionTask)?.resume() } }
}
/// Extend URLSessionTask to be an AsyncTask
extension URLSessionTask: AsyncTask {
public var isCancelled: Bool {
return state == .canceling || (error as NSError?)?.code == NSURLErrorCancelled
}
}
/// Add async version of dataTask(with:) which uses suspendAsync to handle the callback
extension URLSession {
func dataTask(with request: URLRequest) async -> (request: URLRequest, response: URLResponse, data: Data) {
return await suspendAsync { continuation, error, task in
let dataTask = self.dataTask(with: request) { data, response, err in
if let err = err {
error(err)
} else if let response = response, let data = data {
continuation((request, response, data))
}
}
task(dataTask)
dataTask.resume()
}
}
}
Example usage for async URLSession.dataTask:
func performAppleSearch() async -> String {
let urlSession = URLSession(configuration: .default)
let request = URLRequest(url: URL(string: "https://itunes.apple.com/search")!)
let result = await urlSession.dataTask(with: request)
if let resultString = String(data: result.data, encoding: .utf8) {
return resultString
}
throw WebResourceError.invalidResult
}
// Execute the URLSession example
do {
let chain = try beginAsyncTask {
let result = try performAppleSearch()
print("Apple search result: \(result)")
}
...
chain.suspend()
...
chain.cancel()
} catch {
print("Apple search error: \(error)")
}
More detailed information can be found in the gist and in the prototype.
Cancellation could be implemented separately, but including it with Swift Async/Await would encourage authors of coroutines to provide an AsyncTask to cancel the coroutine.
I'd love to get feedback and suggestions!!
Doug