使用 continuation 改写函数

背景介绍

在 async 关键字被引入之前,我们常用回调和 delegate 方法来处理和响应异步事件。当我们希望保留原有函数(或者原有函数无法被改写,比如在使用第三方库时),创建一个异步函数原有函数的异步函数版本时,就可以用 continuation 来进行改写。
所谓的 continuation(续体)指的是一个函数被分割后的一部分拥有独立存储空间的执行单元。它是我们构建一个异步函数的桥梁。

改写方法

Swift 提供了以下四种全局函数用以转换成异步函数:
func withUnsafeContinuation<T>
func withUnsafeThrowingContinuation<T>
func withCheckedContinuation<T>
func withCheckedThrowingContinuation<T>
举个例子,假如我们想把下面这个函数转化为异步函数:
func IOoperation(completion: @escaping (String) -> Void) {
    let queue = DispatchQueue(label: "IOQueue.myApp")
    // 模拟耗时操作
    queue.asyncAfter(deadline: .now() + 3.0) {
        completion("Hello")
    }
}
IOoperation 是一个典型的使用回调响应异步事件的函数,这样的函数可能会遇到回调地狱、错误调用等问题。要借助 continuation 把它改写成异步函数,只需这样:
func IOoperation() async -> String {
    await withUnsafeContinuation { continuation in
        IOoperation { str in
            continuation.resume(returning: str)
        }
    }
}
使用方法从:
IOoperation { str in
		print(str)
}
变成了:
let t = Task {
		let IOStr = await IOoperation()
		print(IOStr)
}
带有 Throwingwith…Continuation 函数代表函数可能会抛出异常值,如函数:
func IOoperationWithError(completion: @escaping (String?, Error?) -> Void) {
    
    let queue = DispatchQueue(label: "IOoperationWithErrorQueue")
    
    queue.asyncAfter(deadline: .now() + 3.0) {
        completion("Hi", nil)
    }
}
withUnsafeThrowingContinuation 改写为:
func IOoperationWithError() async throws -> String? {
    try await withUnsafeThrowingContinuation{ continuation in
        IOoperationWithError { (Str, Err) in
            if let Err {
                continuation.resume(throwing: Err)
            } else if let Str {
                continuation.resume(returning: Str)
            } else {
                assertionFailure("Both parameters are nil")
            }
        }
    }
}
在上面的例子中,可以看到我们对于函数的返回值或者 Error 都使用了 resume() 方法来进行捕获。
当我们在一个异步函数中调用 with…Continuation 函数时,这个异步函数将会被暂停, resume() 方法会在之前的暂停点上恢复这个异步函数(即回到原线程上继续执行),并捕获它的返回值或者 Error。这就是我们在异步函数和同步函数之间进行交互的机制。

Unsafe 与 Checked 的区别

在官方文档中,有一条重要提示:您程序的每个执行路径上必须调用且仅调用一次 resume() 方法。
这是因为,如果从不调用 resume() 方法,任务将永远处于挂起状态,从而可能泄露与之关联的资源;多次调用 resume() 方法,也是未定义的行为,同样可能出现内存泄露等错误导致程序崩溃。
带有 Checkedwith…Continuation 函数,在使用时编译器会保证我们调用且仅调用一次 resume() 方法,而带有 Unsafewith…Continuation 函数,编译器将不作检查,需要我们自己确保正确地调用 resume()
但是,前者依赖于运行时检查,在对性能要求特别高的场景下,UnsafeContinuation 可以获得更小的性能开销。总而言之,无论使用何种 continuation,都必须小心对待 resume() 方法的使用。
 
你觉得这篇文章怎么样?
YYDS
比心
加油
菜狗
views

Loading Comments...