Swift:异步/等待

2020-12-05 18:05:08

现代Swift开发涉及大量使用闭包和完成处理程序的异步(或async")编程,但是这些API很难使用。当使用许多异步操作,需要错误处理或异步调用之间的控制流程变得复杂时,这尤其成问题。该建议描述了一种语言扩展,以使其更加自然和更少出错。

该设计向Swift引入了协程模型。函数可以选择异步,从而允许程序员使用常规的控制流机制来编写涉及异步操作的复杂逻辑。编译器负责将异步函数转换为适当的闭包和状态机集。

该提议定义了异步函数的语义。但是,它不提供并发性:引入结构化并发性的单独建议涵盖了并发性,该建议将异步功能与并发执行的任务相关联,并提供用于创建,查询和取消任务的API。

使用显式回调(也称为完成处理程序)的异步编程存在许多问题,我们将在下面进行探讨。我们建议通过将异步函数引入该语言来解决这些问题。异步功能允许将异步代码编写为直线代码。它们还允许实现直接推理代码的执行模式,从而使回调运行得更加有效。

简单异步操作序列通常需要深度嵌套的闭包。这是一个显示此内容的虚构示例:

func processImageData1(completeBlock:(_ result:Image)-> Void){loadWebResource(" dataprofile.txt"){loadWebResource(" imagedata.dat")中的dataResource {imageResource在解码图像(dataResource,imageResource)中{在dewarpAndCleanupImage(imageTmp)中的imageTmp {在completionBlock(imageResult)中的imageResult}}}}}}} processImageData1 {display(image)中的图像}

这个“厄运金字塔”使得难以阅读和跟踪代码的运行位置。另外,必须使用一堆闭包会导致许多二阶效果,我们将在后面讨论。

回调使错误处理变得困难且非常冗长。 Swift 2引入了用于同步代码的错误处理模型,但是基于回调的接口并未从中获得任何好处:

//(2a)对每个回调使用`guard`语句:func processImageData2a(completeBlock:(_ result:Image?,_ error:Error?)->无效){loadWebResource(" dataprofile.txt&# 34;){dataResource,后卫的错误让dataResource = dataResource else {completeBlock(nil,错误)返回} loadWebResource(" imagedata.dat"){imageResource,后卫的错误let imageResource = imageResource else {completeBlock (nil,错误)返回} encodeImage(dataResource,imageResource){imageTmp,后卫发生错误,使imageTmp = imageTmp else {completeBlock(nil,错误)返回} defwarpAndCleanupImage(imageTmp){imageResult,后卫发生错误,使imageResult = imageResult else { (nil,错误)返回} completeBlock(imageResult)}}}}} processImageData2a {图片,后卫错误让image =图片else {display(今天没有图片&#34 ;,错误)返回} display(image )}

将结果添加到标准库中,改进了Swift API的错误处理。异步API是导致Result的主要动力之一:

//(2b)对每个回调使用`do-catch`语句:func processImageData2b(completeBlock:(Result< Image,Error>)->无效){loadWebResource(" dataprofile.txt") {做中的dataResourceResult {让dataResource =尝试dataResourceResult。 get()loadWebResource(" imagedata.dat"){do中的imageResourceResult {让imageResource =尝试imageResourceResult。 get()encodeimage(dataResource,imageResource){在do中的imageTmpResult {让imageTmp =尝试imageTmpResult。 get()dewarpAndCleanupImage(imageTmp){imageResult in progressBlock(imageResult)}} catch {completeBlock(。failure(error))}}} catch {completeBlock(。failure(error))}}} catch { ))}}} processImageData2b {产生结果{产生图像=尝试结果。 get()display(image)} catch {display("今天没有图像&#34 ;,错误)}}

//(2c)对每个回调使用`switch`语句:func processImageData2c(completeBlock:(Result< Image,Error>)->无效){loadWebResource(" dataprofile.txt"){dataResourceResult在switch dataResourceResult {case。 success(let dataResource):loadWebResource(" imagedata.dat"){开关imageResourceResult中的imageResourceResult {case。 success(let imageResource):decodeImage(dataResource,imageResource){imageTmpResult在切换imageTmpResult {case。 success(let imageTmp):dewarpAndCleanupImage(imageTmp){imageResult in completeBlock(imageResult)} case。失败(让错误):completionBlock(。失败(错误))}} case。失败(让错误):completionBlock(。失败(错误))}} case。 failure(let error):completeBlock(。failure(error))}}} processImageData2c {导致切换结果{case。 success(let image):display(image)case。 failure(let error):显示("今天没有图像&#34 ;,错误)}}

有条件地执行异步函数是一个巨大的痛苦。例如,假设我们需要" swizzle"获得图像后。但是,有时我们必须进行异步调用才能解码图像,然后才能产生混乱。构造此函数的最佳方法也许是在辅助程序中编写复杂的代码。在完成处理程序中有条件捕获的闭包,如下所示:

func processImageData3(收件人:Person,completionBlock:(_结果:Image)->无效){让swizzle:((_ content:Image)->无效= {// ...连续闭合,最终会调用completionBlock},如果是接收者。 hasProfilePicture {swizzle(recipient。profilePicture)}否则{解码图像{swizzle(image)中的图像}}}

这种模式会颠倒函数的自然自上而下的组织方式:将在函数后半部分执行的代码必须出现在上半部分执行的部分之前。除了重组整个函数之外,我们现在还必须仔细考虑延续闭包中的捕获,因为闭包用于完成处理程序中。随着条件执行的异步函数数量的增加,问题变得更加严重,从而产生了本质上是倒置的“厄运金字塔”。

通过简单地返回而不调用正确的完成处理程序块,可以很容易地提早解决异步操作。当被遗忘时,这个问题很难调试:

func processImageData4a(completionBlock:(_结果:图片?,_错误:错误?)->无效){loadWebResource(" dataprofile.txt"){dataResource,警惕中的错误让dataResource = dataResource else {返回//<--忘记调用该块} loadWebResource(" imagedata.dat"){imageResource,防护错误让imageResource = imageResource否则{返回//<--忘记调用该块} ...}}}

当您确实记得调用该块时,您仍然可以忘记之后返回:

func processImageData4b(收件人:人,completionBlock:(_结果:图像?,_错误:错误?)->无效){如果是收件人。 hasProfilePicture {如果让图片=收件人。 profilePicture {completeBlock(image)//<-调用块后忘记返回}} ...}

值得庆幸的是,防护语法可以防止忘记返回某种程度,但这并不总是相关的。

这很难量化,但是作者认为,定义和使用异步API(使用完成处理程序)的尴尬已导致许多具有明显同步行为的API被定义,即使它们可以阻塞也是如此。这可能会导致UI应用程序中的性能和响应性问题,例如旋转的光标。当异步对于实现规模扩展至关重要时,也可能导致无法使用的API的定义。在服务器上。

Warning: Can only detect less than 5000 characters

挂起点是异步函数执行中必须放弃其线程的点。悬挂点始终与函数中的确定性,句法明确的事件相关;从函数的角度来看,它们永远不会被隐藏或异步。挂起点的主要形式是对与不同执行上下文关联的异步函数的调用。

重要的是,暂停点仅与显式操作相关联。实际上,此提议要求将可能暂停的调用包含在await表达式中,这一点非常重要。这是Swift的先例,该先例要求try表达式包含对可能引发错误的函数的调用。标记悬浮点特别重要,因为悬浮会中断原子性。例如,如果异步功能在受串行队列保护的给定上下文中运行,则到达挂起点意味着可以在同一串行队列上交错其他代码。关于原子性的一个经典但有点老套的例子是对银行进行建模:如果将存款记入一个帐户,但操作在处理匹配的提款之前挂起,则会创建一个窗口,可以对这些资金进行两次花费。对于许多Swift程序员来说,更紧密的示例是UI线程:暂停点​​是可以向用户显示UI的点,因此,构建其部分UI并随后暂停的程序可能会出现闪烁的,部分构造的UI。 (请注意,还使用显式回调在代码中显式调用了挂起点:挂起发生在外部函数返回的点与回调开始运行之间。)要求所有潜在的挂起点都已标记,以便程序员可以安全地假定没有潜在的悬浮点将具有原子性,并且更容易识别出有问题的非原子模式。

由于挂起点只能出现在异步函数中显式标记的点上,因此长时间的计算仍会阻塞线程。当调用仅执行大量工作的同步函数时,或者遇到直接用异步函数编写的特别密集的计算循环时,可能会发生这种情况。无论哪种情况,线程都无法在这些计算运行时交错代码,这通常是正确性的正确选择,但也可能成为可伸缩性问题。需要进行大量计算的异步程序通常应在单独的上下文中运行它。如果不可行,则可以使用图书馆设施来人为地暂停并允许其他操作进行交错。

异步函数应避免调用实际上可能阻塞线程的函数,尤其是当它们可以阻塞线程以等待无法保证当前正在运行的工作时。例如,获取互斥锁只能阻塞,直到某个当前正在运行的线程放弃该互斥锁为止。这有时是可以接受的,但必须小心使用,以免引起死锁或人为的可伸缩性问题。相反,等待条件变量可能会阻塞,直到安排了一些其他工作来发出信号为止。这种模式强烈反对推荐。需要进行持续的库工作以提供允许程序避免这些陷阱的抽象。

类老师{init(hiringFrom:College)异步抛出{...}私有func raiseHand()异步->布尔{...}}

原理:异步跟随参数列表,因为它是函数类型及其声明的一部分。这遵循引发掷球的先例。

对声明为异步的函数或初始化程序的引用的类型是异步函数类型。如果该引用是对实例方法的“咖喱”静态引用,则它是" inner"。异步的函数类型,与此类引用的通常规则一致。

诸如deinit和storage访问器(即属性和下标的getter和setter)之类的特殊功能不能异步。

基本原理:仅具有getter的属性和下标可能是异步的。但是,还具有异步setter的属性和下标意味着能够将引用作为inout传递并深入到该属性本身的属性,这取决于setter有效地是“即时”实体。 (同步,非抛出)操作。禁止异步属性是比仅允许仅获取异步属性和下标更简单的规则。

如果一个函数既是异步函数又是引发函数,那么async关键字必须在类型声明中的引发函数之前。如果异步并重新抛出,则适用相同的规则。

理由:这种顺序限制是任意的,但不是有害的,并且消除了进行样式辩论的可能性。

仅当超类具有零参数,同步的指定初始化器时,具有超类但未调用超类初始化器的类的异步初始化器才会获得对super.init()的隐式调用。

基本原理:如果超类初始化程序是异步的,则对异步初始化程序的调用是潜在的挂起点,因此,该调用(和必需的等待)必须在源中可见。

异步函数类型与同步函数类型不同。但是,存在从同步功能类型到其相应异步功能类型的隐式转换。这类似于从非抛出函数到其抛出对应项的隐式转换,后者也可以与异步函数转换组成。例如:

struct FunctionTypes {var syncNonThrowing:()-> 无效var syncThrowing:()抛出-> 无效var asyncNonThrowing:()async-> 无效var asyncThrowing:()异步抛出-> 无效的变异函数showConverts(){//可以添加' async' 和/或引发 asyncNonThrowing = syncNonThrowing asyncThrowing = syncThrowing syncThrowing = syncNonThrowing asyncThrowing = asyncNonThrowing //错误 ......