The Cancellation
library enables any cancelable task and even quite complex systems of asynchronous tasks to be cancelled in a safe and effective manner.
The approach chosen to solve this task separates the perspective of a client which creates a task and possibly wants to cancel it later and the perspective of the task which needs to get notified about the cancellation request.
For the perspective of the client the library provides the class CancellationRequest
. A client simply creates an instance using the default initializer:
self.cancellationRequest = CancellationRequest()
which then can be used later to perform a "cancellation request":
self.cancellationRequest.cancel()
Now, in order associate a cancelable asynchronous task with this cancelation request, the cancellation request has one Cancellation Token. This cancellation token can be used to register one or more cancellation handlers or it can be queried about its state, that is, obtain a boolean value which indicates that the client has requested a cancellation. The cancellation request's cancellation token is passed as a parameter to a function that starts its underlying asynchronous task:
Note:
The library exposes a Cancellation Token as a protocolCancellationTokenType
.
let cr = CancellationRequest()
task(param: param, cancellationToken: cr.token) { (result, error) in
...
}
The implementation of the above function task
must of course monitor the state of the token, so when the client requested a cancellation, the state of the token changes to "cancelled", and the task should cancel its underlying operation.
Basically, there are two ways to achieve this:
The cancellation token has a property isCancelled
. It becomes true
when the client requested a cancellation. The task must periodically query the property and then abort the operation if isCancelled
returns true
.
A Cancellation Token can register one or more "handlers". Actually, there are a few ways to register a handler, onCancel
is the most straight forward one. The handler will be called when the client has requested a cancellation. This can be utilized to cancel the underlying task.
A handy URLSession
extension is a perfect example to illustrate the second approach number:
extension URLSession {
func data(from url: URL, cancellationToken: CancellationTokenType, completion: @escaping (Data?, URLResponse?, Error?) -> ()) {
let task = self.dataTask(with: url) { data, response, error in
completion(data, response, error)
cancellationToken.onCancel { [weak task] in
task?.cancel()
}
task.resume()
}
}
We should notice, that in order to not keeping a reference to the data task longer than necessary, it is important, that the cancellation handler weakly captures the data task reference.
The above rule might be a good practice, but it is difficult to enforce. Due to this, there are further ways to register a handler. Actually, there is a slightly better way to implement the above extension, which is shown further below.
Add
github "couchdeveloper/Cancellation"
to your Cartfile.
In your source files, import the library as follows
import Cancellation
Add the following line to your Podfile:
pod 'Cancellation'
In your source files, import the library as follows
import Cancellation
To use SwiftPM, add this to your Package.swift:
.Package(url: "https://github.com/couchdeveloper/Cancellation.git")
Here, we define a handy extension for URLSession
to perform a "GET" request with a function data
having a cancellation token as an additional parameter.
In order to accomplish this, we implement the monitoring of the token with a function register
. This takes a Cancelable
as a parameter. A Cancelable
is a protocol which declares just one function func cancel()
. A URLSessionTask
already naturally conforms to this protocol, we just need to declare it. Using register
over onCancel
has the benefit that we do not need to implement a handler at all and thus, since there is no handler we also don't have to take care of the fact that the task should be captured weakly within the handler.
extension URLSessionTask: Cancelable {}
extension URLSession {
func data(from url: URL, cancellationToken: CancellationTokenType, completion: @escaping (Data?, URLResponse?, Error?) -> ()) {
let task = self.dataTask(with: url) { data, response, error in
completion(data, response, error)
}
cancellationToken.register(cancelable: task)
task.resume()
}
}
Suppose, you want to issue a network request from your view controller. You have defined an instance value, like so
var cancellationRequest = CancellationRequest()
Then use it as follows:
self.cancellationRequest = CancellationRequest() // invalidate any previous obsolete cancellation handlers
URLSession.shared.data(from: url, cancellationToken: self.cancellationRequest.token) { data, response, error in
// handle (data, response, error)
...
}
and possibly, you may want to cancel this request (or any other tasks monitoring the cancellation token):
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.cancellationRequest.cancel()
}
A cancellation token has two states:
That is, its "value" is either "undetermined" or it is a Boolean whose value is either true
or false
.
A cancellation token starts out to be in state "undetermined". Eventually it will be completed with a boolean value, where true
means "cancelled" and false
means, well "not cancelled". It may sound strange that a cancellation token can have a state "not cancelled", but thinking further, it makes absolute sense:
Suppose the client did not request a cancellation when it ceases to exist, and its cancellation request value will be deallocated as well. At this point, the cancellation request will complete its cancellation token with value false
indicating that at this time on there can never be a cancellation request anymore. When the token will be completed, it resumes registered handlers while skipping those which explicitly registered to execute only when the state equals "cancelled". Other handlers, for example those registered with onComplete
will now execute.
So, once a token is completed, all registered handlers will eventually be deallocated, which in turn will release resources, including those captured in the handlers itself.
Once a token has been completed, it can never be changed anymore. We can still register handlers, but they will either run asynchronously or be skipped immediately, depending on the state and the type of the register function.
A Combinator is an instance function which returns a new instance of the same type. Combinators can be used to build more complex systems.
The Cancellation Token defines a few combinators:
func map(f: @escaping () -> (Bool)) -> CancellationToken
andfunc flatMap(f: @escaping () -> (CancellationTokenType)) -> CancellationToken
The function f
will be called when the cancellation token has been cancelled.
map
returns a token that will be completed with the return value of the transform function f
. That is, if f
returns false
, the returned token will be completed with "not cancelled". Otherwise, it will be completed with "cancelled".
flatMap
returns a token that will be completed with the eventual value of the returned token from the transform function f
.
Implement a function &&
, which returns a new token which semantically defines AND-ing two cancellation tokens.
An implementation might look as follows:
public func && (left: CancellationTokenType, right: CancellationTokenType)
-> CancellationTokenType
{
return left.flatMap {
// executes only when left has been cancelled.
right.map {
// executes only when right has been cancelled.
true // completes the returned token with true (cancelled)
}
}
}
link |
Stars: 3 |
Last commit: 3 years ago |
Swiftpack is being maintained by Petr Pavlik | @ptrpavlik | @swiftpackco | API | Analytics