大师学SwiftUI第9章Part 1 - 异步并发之Task、Async、Await和错误

简介: 苹果系统借助现代处理器的多核可同步执行多条代码,提升同一时间内程序所能执行的任务。例如,一段代码从网上下载文件,另一段代码可以在屏幕上显示进度。此时,我们不能等待第一个执行完后再执行第二个,而必须要同步执行这两个任务。

苹果系统借助现代处理器的多核可同步执行多条代码,提升同一时间内程序所能执行的任务。例如,一段代码从网上下载文件,另一段代码可以在屏幕上显示进度。此时,我们不能等待第一个执行完后再执行第二个,而必须要同步执行这两个任务。

要并行处理代码,系统将代码单元分组成任务。在Swift中,任务可以通过异步和并发编程实现。异步编程是一种编程模式,代码在完成任务前等待处理完成。这样系统可以在不同进程间共享计算资源。等待期间,系统可使用资源执行其它任务。而并发编程实现的代码可以利用多核同步执行多个任务。

图9-1:异步和并发编程

因很多应用可以同时运行,系统并不会对每个应用分配指定的核数。系统会创建一些执行线程,将任务分配给这些线程,然后根据可用资源决定哪个核执行哪些线程。在图9-1的示例中,左边是一个异步任务,从网上加载图片然后在屏幕上显示。在等待服务响应时,线程处于空闲状态可以执行其它任务,因此系统可以使用它执行更新进度条的任务。右图中创建了并发任务,因此在不同进程中同步执行。

任务

异步和并发的代码由任务定义。Swift标准库中包含有Task结构体用于创建和管理这些任务。下面是结构体的初始化方法:

  • Task(priority: TaskPriority?, operation: Closure):这个初始化方法创建并运行新任务。priority参数是一个辅助系统决定何时执行任务的结构体。这一结构体中包含类型属性定义标准优先级。当前有backgroundhighlowmediumuserInitiatedutilityoperation参数是一个闭包,内含任务执行的语句。

Task结构带有如下属性用于取消任务。

  • isCancelled:该属性返回一个表示任务是否被取消的布尔值。
  • cancel():取消任务的方法。

还有一些类型属性和方法,可用于从当前任务获取信息或创建执行指定处理的任务。以下是最常用的。

  • currentPriority:该属性返回当前任务的优先级。这是一个TaskPriority结构体,有属性backgroundhighlowmediumuserInitiatedutility
  • isCancelled:该属性返回一个表示当前任务是否取消的布尔值。
  • sleep(nanoseconds: UInt64):本方法按照nanoseconds参数指定的时间挂起当前任务。

虽然可以在代码的任意地方创建Task结构体初始化异步任务,SwiftUI自带了如下的修饰符在视图出现时进行创建。

  • task(priority: TaskPriority, Closure):此修饰符在视图出现时执行第二个参数所指定的任务。priority参数是一个结构体,辅助系统决定何时执行任务。值有backgroundhighlowmediumuserInitiatedutility
  • task(id: Value, priority: TaskPriority, Closure):此修饰符在视图出现时执行第三个参数所指定的任务。id参数是用于标识任务的值。每当这个值发生改变时,任务就会重启。priority参数是一个结构体,辅助系统决定何时执行任务。值有backgroundhighlowmediumuserInitiatedutility

Async和Await

异步和并发任务在Swift中通过asyncawait关键字定义。例如要创建异步任务,我们使用async标注方法,然后使用await等待该方法执行完成。这表示在另一异步方法内只能通过await关键字调用一个异步方法,创建一个无限循环。开启这一循环,我们使用task()修饰符在视图出现时初始化异步任务,如下所示。

示例9-1:初始化异步任务

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, world!")
                .padding()
        }
        .task(priority: .background) {
            let imageName = await loadImage(name: "image1")
            print(imageName)
        }
    }
    func loadImage(name: String) async -> String {
        try? await Task.sleep(nanoseconds: 3 * 1000000000)
        return "Name: \(name)"
    }
}

本例中使用background优先级创建任务,表示它相对其它并行任务不具备优先级。在闭包中,我们调用loadImage()方法,然后在控制台中打印返回值。我们定义的这个方法模拟从网上下载图片。稍后我们会学习如何下载数据及连接网络,但这里我们使用了sleep()方法让任务暂停3秒,假装在下载图片(方法接收值的单位是纳秒)。停顿结束后,方法返回带文件名的字符串。要以异步定义方法,我们在参数的后面添加async关键字,然后使用await关键字调用它,表示任务必须等待处理完成。

task()修饰符创建任务并添加到线程中。在视图加载后,会执行赋值给修饰符的闭包。在闭包中,我们调用loadImage()方法,等待其完成。方法停顿3秒、返回字符串。此后,任务继续执行语句,在控制台上打印消息。

✍️跟我一起做:创建一个多平台项目。使用示例9-1中的代码更新ContentView结构体。在模拟器中运行应用。3秒后会看到控制中打印的消息。

一个任务可执行多个异步处理。例如,下例中调用了loadImage()3次来下载3张图片。

示例9-2:运行多异步处理

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, world!")
                .padding()
        }
        .task(priority: .background) {
            let imageName1 = await loadImage(name: "image1")
            let imageName2 = await loadImage(name: "image2")
            let imageName3 = await loadImage(name: "image3")
            print("\(imageName1), \(imageName2), \(imageName3)")
        }
    }
    func loadImage(name: String) async -> String {
        try? await Task.sleep(nanoseconds: 3 * 1000000000)
        return "Name: \(name)"
    }
}

这些处理逐条按顺序执行。任务会等待上一条处理结束再处理下一条。本例中,整个任务耗时9秒完成(每个处理3秒)。

✍️跟我一起做:使用示例9-2中的代码更新ContentView结构体。在模拟器中运行应用。9秒后会看到控制台中打印的消息。

只需在视图加载后运行异步任务使用task()修饰符很有用,但大多数时候任务和视图的生命周期无依赖关系,必须使用Task初始化方法显式地创建。比如可以通过onAppear()方法和Task结构体来重现上例。

示例9-3:显式定义任务

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, world!")
                .padding()
        }
        .onAppear {
            Task(priority: .background) {
                let imageName1 = await loadImage(name: "image1")
                let imageName2 = await loadImage(name: "image2")
                let imageName3 = await loadImage(name: "image3")
                print("\(imageName1), \(imageName2), \(imageName3)")
            }
        }
    }
    func loadImage(name: String) async -> String {
        try? await Task.sleep(nanoseconds: 3 * 1000000000)
        return "Name: \(name)"
    }
}

这一视图和之前一样执行了3条处理,但这里显式地定义了任务,我们有了更多的控制权。比如,现在可以将任务赋值给变量,然后调用cancel()方法取消任务。

cancel()方法用于取消任务,便处理不会自动取消,我们必须使用isCancelled属性监测任务是否被取消并自行停止任务,如下例所示。

示例9-4:取消任务

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, world!")
                .padding()
        }
        .onAppear {
            let myTask = Task(priority: .background) {
                let imageName = await loadImage(name: "image1")
                print(imageName)
            }
            Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { (timer) in
                print("The time is up")
                myTask.cancel()
            }
        }
    }
    func loadImage(name: String) async -> String {
        try? await Task.sleep(nanoseconds: 3 * 1000000000)
        if !Task.isCancelled {
            return "Name: \(name)"
        } else {
            return "Task Cancelled"
        }
    }
}

本例中将前面的任务赋值给了一个常量,然后创建一个定时器在2秒后调用任务的cancel()方法。在loadImage()方法中,我们读取isCancelled属性进行相对应的响应。如果取消了任务,返回Task Cancelled,否则和之前一样返回名称。注意本例我们是在任务执行的处理内操作,因此使用了类型属性,而不是实例属性(我们从数据类型而不是实例中读取isCancelled属性)。该属性根据当前任务的状态返回truefalse。任务在完成后就被取消了。

任务可接收并返回值。Task结构体包含一个value属性提供对任务返回值的访问。当然,我们需等待任务完成才能读取值,如下例如下。

示例9-5:读取任务的返回值

struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, world!")
                .padding()
        }
        .onAppear {
            Task(priority: .background) {
                let imageName = await loadImage(name: "image1")
                print(imageName)
            }
        }
    }
    func loadImage(name: String) async -> String {
        let result = Task(priority: .background) { () -> String in
            let imageData = await getMetadata()
            return "Name: \(name) Size: \(imageData)"
        }
        let message = await result.value
        return message
    }
    func getMetadata() async -> Int {
        try? await Task.sleep(nanoseconds: 3 * 1000000000)
        return 50000
    }
}

因为我们需要等待任务完成才能使用值,所以定义了第二个任务。处理和之前一样启动,通过任务调用loadImage()方法,但现在创建了第二个返回字符串的任务。该任务执行另一个异步方法,等待3秒、返回数字50000。在处理结束后,任务使用名称和返回的数字创建字符串。然后通过value属性获取字符串,将其返回给原始任务,打印到控制台。

至此,我们使用了异步方法,但还可以定义异步属性。只需要使用async关键字定义getter。

示例9-6:定义异步属性

struct ContentView: View {
    var thumbnail: String {
        get async {
            try? await Task.sleep(nanoseconds: 3 * 1000000000)
            return "mythumbnail"
        }
    }
    var body: some View {
        VStack {
            Text("Hello, world!")
                .padding()
        }
        .onAppear {
            Task(priority: .background) {
                let imageName = await thumbnail
                print(imageName)
            }
        }
    }
}

这次把调用方法改成由任务读取属性。属性在挂起任务3秒后返回字符串。这里做挂起只是为了进行演示,但在这个属性中可以执行任意需要的任务,比如处理或下载数据。

错误

异步任务不一定都能成功,所以必须准备好处理返回的错误。如果在创建自己的任务,可以通过实现Error协议的枚举来定义错误,在第3章中进行过讲解(见示例3-189)。下例中定义了一个含两个错误的结构体,一个在未找到服务端元数据(noData)时返回,另一个在图片不存在(noImage)时返回。

示例9-7:响应错误

enum MyErrors: Error {
    case noData, noImage
}
struct ContentView: View {
    var body: some View {
        VStack {
            Text("Hello, world!")
                .padding()
        }
        .onAppear {
            Task(priority: .background) {
                do {
                    let imageName = try await loadImage(name: "image1")
                    print(imageName)
                } catch MyErrors.noData {
                    print("Error: No Data Available")
                } catch MyErrors.noImage {
                    print("Error: No Image Available")
                }
            }
        }
    }
    func loadImage(name: String) async throws -> String {
        try? await Task.sleep(nanoseconds: 3 * 1000000000)
        let error = true
        if error {
            throw MyErrors.noImage
        }
        return "Name: \(name)"
    }
}

上例中的loadImage()在测试代码时总是会抛出noImage错误。其中的任务通过do catch语句检测错误并在控制台打印消息报告错误。注意在异步方法可能会抛出错误时,必须在async后使用关键字throws进行声明。


其它相关内容请见虚拟现实(VR)/增强现实(AR)&visionOS开发学习笔记

相关文章
|
12天前
|
JavaScript 前端开发
js开发:请解释什么是ES6的async/await,以及它如何解决回调地狱问题。
ES6的async/await是基于Promise的异步编程工具,简化了代码并提高可读性。它避免回调地狱,将异步操作转化为Promise,使得代码同步化。错误处理更直观,无需嵌套回调或.then()。
16 1
|
28天前
|
前端开发
Await和Async是什么?跟Promise有什么区别 使用它有什么好处
Await和Async是什么?跟Promise有什么区别 使用它有什么好处
|
API 数据库
FastAPI(63)- Concurrency and async / await 并发、异步/等待
FastAPI(63)- Concurrency and async / await 并发、异步/等待
589 0
|
2月前
|
C#
C#学习系列相关之多线程(四)----async和await的用法
C#学习系列相关之多线程(四)----async和await的用法
|
2月前
|
API
es6读书笔记(六) async await
es6读书笔记(六) async await
|
5月前
|
监控 前端开发 JavaScript
async/await:使用同步的方式去写异步代码
async/await:使用同步的方式去写异步代码
55 1
|
8月前
|
前端开发
前端(十二)——深入理解和使用 async和await
前端(十二)——深入理解和使用 async和await
|
消息中间件 前端开发 JavaScript
ES8 中的 async/await —— 异步函数
ES8 中的 async/await —— 异步函数
151 0
|
前端开发
前端学习案例11-async和await之1
前端学习案例11-async和await之1
42 0
前端学习案例11-async和await之1