Cancellation
概览
Cancellation
库允许任何可取消的任务以及相当复杂的异步任务系统以安全有效的方式进行取消。
解决此问题的方法选择了客户端的视角,该客户端创建任务并在以后可能希望取消它,以及需要关于取消请求通知的任务的视角。
对于客户端的视角,库提供了一个名为 CancellationRequest
的类。客户端只需要使用默认初始化器创建一个实例
self.cancellationRequest = CancellationRequest()
然后可以使用它来执行“取消请求”
self.cancellationRequest.cancel()
现在,为了将可取消的异步任务与该取消请求关联起来,该取消请求有一个 Cancellation Token。该取消令牌可用于注册一个或多个取消处理程序或查询其状态,即获取一个布尔值,表示客户端已请求取消。取消请求的取消令牌作为参数传递给一个函数,该函数启动其底层异步任务
注意
该库将 Cancellation Token 作为协议CancellationTokenType
公开。
let cr = CancellationRequest()
task(param: param, cancellationToken: cr.token) { (result, error) in
...
}
上面函数task
的实现必须当然监控令牌的状态,所以当客户端请求取消时,令牌的状态变为“已取消”,任务应该取消其底层操作。
基本上,有两种方法可以实现这一点
- 轮询
取消令牌有一个名为 isCancelled
的属性。在客户端请求取消时,它变为 true
。任务必须定期查询该属性,如果 isCancelled
返回 true
,则终止操作。
- 注册一个 Cancellation Handler
取消令牌可以注册一个或多个“处理器”。实际上,注册处理器有多种方法,其中onCancel
是最直接的一种。当客户端请求取消时,将调用处理器。这可以用来取消底层任务。
一个方便的URLSession
扩展是说明第二种方法的绝佳示例。
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()
}
}
我们应该注意,为了避免不必要地长时间保留数据任务的引用,很重要的一点是取消处理器应该是弱引用数据任务引用。
上述规则可能是良好的实践,但很难强制执行。因此,还有其他注册处理器的方法。实际上,有一种稍微更好的方法来实现上述扩展,如下所示。
安装
Carthage
添加
github "couchdeveloper/Cancellation"
到您的Cartfile。
在您的源文件中,按照以下方式导入库
import Cancellation
CocoaPods
将以下行添加到您的 Podfile
pod 'Cancellation'
在您的源文件中,按照以下方式导入库
import Cancellation
SwiftPM
要使用SwiftPM,请将以下内容添加到您的Package.swift
.Package(url: "https://github.com/couchdeveloper/Cancellation.git")
用法
示例
在这里,我们对URLSession
定义了一个实用的扩展,用具有取消标记参数的函数data
来执行“GET”请求。
为了实现这一点,我们通过register
函数来监控该标记。这个函数接收一个Cancelable
作为参数。一个Cancelable
是一个只声明一个函数func cancel()
的协议。一个URLSessionTask
已经自然符合这个协议,我们只需要声明它。使用register
而非onCancel
的好处是,我们无需实现处理程序,因此,由于没有处理程序,我们也不必担心处理程序应该以弱引用的方式捕获任务。
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()
}
}
假设,您想从您的视图控制器中发起一个网络请求。您已经定义了一个实例值,如下:
var cancellationRequest = CancellationRequest()
然后如下使用它
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)
...
}
并且可能,您可能希望取消这个请求(或任何其他正在监控取消标记的任务)
override func viewWillDisappear(_ animated: Bool) {
super.viewWillDisappear(animated)
self.cancellationRequest.cancel()
}
一个取消标记就是Future
取消标记有两种状态
- 未定
- 完成(value: Boo
也就是说,它的“值”要么是“未定”,要么是一个表示布尔值的值,可以是true
或false
。
一个取消标记最初处于“未定”状态。最终它将完成并为布尔值,其中true
表示“已取消”,而false
表示,“未取消”。取消标记可以处于“未取消”的状态听起来可能很奇怪,但思考一下,这绝对是合理的
假设客户端在没有请求取消的情况下停止存在,其取消请求值也会被释放。在这种情况下,取消请求将用值false
完成其取消标记,表示在此时刻不会再有取消请求。在标记完成时,它将恢复已注册的处理程序,而跳过那些明确注册仅在状态等于“已取消”时执行的处理程序。其他处理程序,例如那些使用onComplete
注册的处理程序现在将执行。
因此,一旦标记完成,所有注册的处理程序最终都将被释放,这反过来又会释放资源,包括那些在处理程序中捕获的资源。
一个标记完成后就再也不能改变了。我们仍然可以注册处理程序,但它们要么异步运行,要么根据状态和注册函数的类型立即跳过。
使用组合函数
组合器是一个返回相同类型新实例的实例函数。组合器可以用来构建更复杂的系统。
取消令牌定义了一些组合器
- func map(f: @escaping () -> (Bool)) -> CancellationToken
和 func flatMap(f: @escaping () -> (CancellationTokenType)) -> CancellationToken
当取消令牌被取消时,将调用函数 f
。
map
返回一个令牌,该令牌将使用转换函数 f
的返回值来完成。也就是说,如果 f
返回 false
,则返回的令牌将完成为“未取消”。否则,它将完成为“已取消”。
flatMap
返回一个令牌,该令牌将使用从转换函数 f
返回的令牌的最终值来完成。
可能有助于说明我们如何使用这些组合器的例子
实现一个函数 &&
,它返回一个新令牌,该令牌在语义上定义了两个取消令牌的AND操作。
实现可能看起来如下所示
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)
}
}
}