ZHANGYU.dev

October 14, 2023

Swift Concurrency 学习笔记

Swift5.8 min to read

Swift 5.5里新增了Swift Concurrency,语法和Web前端里的异步非常之像,语法学习起来比较简单。

基本使用

关键词就是asyncawait。不同的是需要放入Task里执行,并且一定需要加await关键字。

func fn() async {    print("async function")}Task {    await fn()}

另外一种是可以抛出错误的async函数。

func fn() async throws -> String{    throw URLError(.badURL)    return "async"}

调用会抛出错误的async函数的时候需要使用try关键字。

Task{    let result = try await fn()    print(result)}

这样是不会输入任何结果的,因为已经抛出错误了,在这种情况需要用do-catch语句。

Task {    do {        let result = try await fn()        print(result)    } catch {        print(error.localizedDescription) // 输出了错误信息    }}

使用do-catch可以捕获错误,另外还有2种try的修饰,try!try?,可以不使用do-catch

let result = try! await fn() // 程序会直接崩溃,不会走do-catch,捕获不了错误print(result)

try!是非常不建议使用的。

let result = try? await fn() // 报错会返回nilprint(result) // nil

try?在出现错误的时候会返回nil,在不需要捕获具体错误信息的时候非常有用。

Task

Task接受一个闭包作为参数,返回一个实例。

取消 Task

Task会返回实例,通过该实例的cancel()方法可取消任务。

func fn() async {    try? await Task.sleep(for: .seconds(2))    print("async function")}let task = Task {    await fn()}task.cancel()

但是实际我们还是会输出"async function",只是跳过了等待2秒。

所以我们需要调用Task.isCancelled或者Task.checkCancellation()来确保不再执行。

func fn() async {    try? await Task.sleep(for: .seconds(2))    if Task.isCancelled { return }    print("async function")}

Task的优先级

Task中有优先级的概念

Task(priority: .background) {    print("background: \(Task.currentPriority)")}Task(priority: .high) {    print("high: \(Task.currentPriority)")}Task(priority: .low) {    print("low: \(Task.currentPriority)")}Task(priority: .medium) {    print("medium: \(Task.currentPriority)")}Task(priority: .userInitiated) {    print("userInitiated: \(Task.currentPriority)")}Task(priority: .utility) {    print("utility: \(Task.currentPriority)")}

输出

medium: TaskPriority(rawValue: 21)high: TaskPriority(rawValue: 25)low: TaskPriority(rawValue: 17)userInitiated: TaskPriority(rawValue: 25)utility: TaskPriority(rawValue: 17)background: TaskPriority(rawValue: 9)

优先级并不一定匹配,有时候会有优先级提升的情况。

子任务会继承父任务的优先级。

Task(priority: .high) {    Task {        print(Task.currentPriority) // TaskPriority(rawValue: 25)    }}

通过Task.detached来分离任务。

Task(priority: .high) {    Task.detached {        print(Task.currentPriority) // TaskPriority(rawValue: 21)    }}

挂起Task

Task.yield()可以挂起当前任务。

Task {    print("task 1")}Task {    print("task 2")}// 输出// task 1// task 2

使用Task.yield()

Task {    await Task.yield()    print("task 1")}Task {    print("task 2")}// 输出// task 2// task 1

async let

await是阻塞的,意味着当前await函数在没执行完之前是不会执行下一行的。

func fn() async -> String {    try? await Task.sleep(for: .seconds(2))    return "async function"}Task {    let result = await fn()    print(result) // 等待两秒后输出async function}

有些情况需要并行运行多个async函数,这个时候则会用到async let

Task {    async let fn1 = fn()    async let fn2 = fn()    let result = await [fn1, fn2]        print(result) // ["async function", "async function"]}

TaskGroup

如果任务过多,或者是循环里创建并行任务,async let就不是那么得心应手了,这种情况我们应该使用withTaskGroupwithThrowingTaskGroup

Task {        let string = await withTaskGroup(of: Int.self) { group in            for i in 0 ... 10 {                group.addTask {                    try? await Task.sleep(for: .seconds(2))                    return i                }            }            var collected = [Int]()            for await value in group {                collected.append(value)            }            return collected        }        print(string)    }

of为子任务返回类型,在TaskGroup里我们也能通过group.cancelAll()group.isCanceled配合来取消任务。

Continuations

Continuations用于将以前的异步回调函数变成async函数,类似前端里的new Promise(resolve,reject)

现有以下代码

func fn(_ cb: @escaping (String) -> Void) {    DispatchQueue.main.asyncAfter(deadline: .now() + 2) {        cb("completed")    }}

这段代码是通过@escaping闭包的形式来获取结果,不能通过await获取,只需要使用withCheckedContinuation就可以将函数改造为async函数。

func asyncFn() async -> String {    await withCheckedContinuation { continuation in        fn { continuation.resume(returning: $0) }    }}Task {  let result = await asyncFn()  print(result)}

除了withCheckedContinuation,还有withCheckedThrowingContinuation可以抛出错误。

actor

在很多语言里,都有线程锁这个概念,避免多个线程同一时间访问同一数据,造成错误。

Swift Concurrency里通过actor来解决这个问题。actor里的属性和方法都是线程安全的。

actor MyActor {    var value:String = "test"        func printValue(){        print(value)    }}

actor内默认属性和方法都是异步的,需要通过await来调用。

Task {    let myActor = MyActor()    await myActor.printValue()    print(await myActor.value)}

如果需要某个方法不用await调用,需要使用nonisolated关键字。

actor MyActor {    nonisolated func nonisolatedFn(){        print("nonisolated")    }}let myActor = MyActor()myActor.nonisolatedFn()

MainActor

现有以下代码

class VM: ObservableObject {    @Published var value = "value"    func change() {        Task{            try? await Task.sleep(for:.seconds(2))            self.value = "change"        }    }}Text(vm.value)    .onTapGesture {        vm.change()    }

当点击Text两秒后会修改值。这时候会提示。

[SwiftUI] Publishing changes from background threads is not allowed; make sure to publish values from the main thread (via operators like receive(on:)) on model updates

因为UI改动都应该发生在主线程,可以使用老办法Dispatch.main.async来解决。在Swift Concurrency里有多个方法。

func change() {    Task {        try? await Task.sleep(for: .seconds(2))        await MainActor.run{            self.value = "change"        }    }}

或者

    func change() {        Task {@MainActor in            try? await Task.sleep(for: .seconds(2))            self.value = "change"        }    }

也可以使用@MainActor将方法或者类标记运行在主队列。

SwiftUI中使用

SwiftUI中直接.task修饰符即可。

Text("Hello World 🌍")    .task {        await fn()    }

同时有一点比较好的是在onDisappear的时候会自动取消Task

结语

作为初学者,Swift Concurrency简化了很多异步相关的问题,不需要再去使用闭包了,不会造成回调地狱,结合SwiftUI使用比Combine更简单友好,非常不错。‘

最近几天学习了这个,虽然我阳了,但是还是顶着发烧总结一晚上,以免烧完已经不记得了。