Swift 5.5里新增了Swift Concurrency,语法和Web前端里的异步非常之像,语法学习起来比较简单。
基本使用
关键词就是async
和await
。不同的是需要放入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
就不是那么得心应手了,这种情况我们应该使用withTaskGroup
和withThrowingTaskGroup
。
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更简单友好,非常不错。‘
最近几天学习了这个,虽然我阳了,但是还是顶着发烧总结一晚上,以免烧完已经不记得了。