探索 Swift Concurrency (1)

Swift 并发是 WWDC 21 上的一个重磅改进,提供了 async/await 的语法,

让代码结构更加流程化,更加便于理解和调试。

官方指南:开始使用 Swift 并发

actor

WWDC Sessions:

利用 Swift 并发消除数据争用

利用 Swift Actor 保护可变状态

actor 和 class 很相似,都是 Ref Type。

但是 Actor 保证了内部数据的唯一性,即一次只有一个函数能调用,

如果有多个函数同时调用该属性,则需要排队

保证了在并发环境下的数据竞争 (Data Races)。

其用法与 struct/class 一致。

Main Actor

WWDC Session:利用 Swift Actor 保护可变状态

简单理解就是:主线程。

所有的 UI Updates 都需要在 Main Actor 环境下完成。

有以下几种写法:

1
2
3
Task { @MainActor in
// UI Updates
}
1
2
3
@MainActor func updateUI() {
// UI Updates
}
1
2
3
4
5
Task { 
await MainActor.run {
// UI Updates
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@MainActor
class ViewModel: ObservableObject {
@Published var show = false

func fetch() async {
let data = ...
updateUI() // Run on the MainActor because the whole class is in MainActor.
}

func updateUI() {
// UI Updates
}
}

async let

WWDC Session:探索 Swift 中的结构化并发

async let 可以让多个操作一起执行,到真正需要使用的时候再 await

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func fetchImage() async {
// Fetching Image
}

func fetchMetaData() async {
// Fetching Meta Data
}

async let image = fetchImage() // Start fetching while the CPU is free.
async let metaData = fetchMetaData() // The same as image.

// Do other work...

// Display the image and backup.
display(await image)
backup(await metaData) // This is just an example...haha

这个例子中,如果只用 await,image 和 metaData 会依次获取,在获取期间程序挂起 (Suspended),不会继续向下执行。

如果 image 的获取比较慢,那么就会卡在这里。

1
2
3
4
5
6
7
8
9
10
11
12
func fetchImage() async { }
func fetchMetaData() async { }

let image = await fetchImage() // It's stuck here.😅
let metaData = await fetchMetaData() // As well as here.😅

// Do other work...
// Because we were stuck before, so we waste a large amount of time.

// Display the image and backup.
display(image)
backup(metaData) // This is just an example...haha

Task Group

WWDC Session:探索 Swift 中的结构化并发

多个任务并发执行的时候可以使用 Task Group

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Throwing Task Group
Task.detached {
await withThrowingTaskGroup(of: Void.self) { group in
for task in self.tasks {
group.addTask {
try await ...
}
}
}
}

// Non-Throwing Task Group
Task.detached {
await withTaskGroup(of: Void.self) { group in
for task in self.tasks {
group.addTask {
await ...
}
}
}
}

TaskGroup 常用在一组数据的处理,例如:获取所有商品的缩略图…

由于有很多 tasks 同时执行,因此就会有潜在的 Data Races

我们不能直接对外部的变量赋值(这是一个编译时检查错误)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var integers = [Int]()

// Throwing Task Group
func foo() {
Task.detached {
try await withThrowingTaskGroup(of: Int.self) { group in
for _ in 0...10 {
group.addTask {
// Return some values here.
return (0...100).randomElement()!
}
}

// Read values from the group.
for try await value in group {
self.integers.append(value)
}
}
}
}

Non-Throwing Task Group 方法一致,这里就不再演示了。

这里使用 Task.detached 是为了防止受到 Context 的影响,

例如:下载所有商品的缩略图应在其他线程上完成,而非主线程。

往下看就知道我在说什么了。

Task vs. Task.detached

WWDC Session:探索 Swift 中的结构化并发

相关文章:What’s the difference between a task and a detached task?

Task 提供了一种 async 的环境来执行异步操作。

Task: Runs the given nonthrowing operation asynchronously as part of a new top-level task on behalf of the current actor.

Task.detached: Runs the given throwing operation asynchronously as part of a new top-level task.

Task 会继承当前的 Context 和 Priority,

Honestly,这句话我理解了很久,Priority好理解,但是 Context 是什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@MainActor
class TaskManager1 {
func doWorks() {
Task {
print("Execute on main thread: \(Thread.isMainThread)") // true
}
Task.detached {
print("Execute on main thread: \(Thread.isMainThread)") // false
}
}
}

class TaskManager2 {
func doWorks() {
Task {
print("Execute on main thread: \(Thread.isMainThread)") // false
}
}
}

诸如线程之类的属性就是所谓的 Context。

也就是说上层的运行环境会被继承到 Task 中来,如果不想要这种继承,使用 Task.detached

需要注意的是,使用 Task.detached 需要显式捕获 self,即 self.variable

Task + ObservableObject

WWDC Session:探索 Swift 中的结构化并发

相关文章:What’s the difference between a task and a detached task?

在 SwiftUI 中,很多情况下我们会使用 @ObservedObject 或者 @StateObject 来管理 ViewModel

这时候,该视图就会在 MainActor 下执行。

1
2
3
4
5
6
7
8
9
10
Task {
for i in 0...1000 {
print("Task 1: \(i)")
}
}
Task {
for i in 0...1000 {
print("Task 2: \(i)")
}
}

上面的例子中,你会发现控制台中 Task 1 和 Task 2 仍然会按次序执行。

这是因为 @ObservedObject 或者 @StateObject 限制了视图运行在 MainActor 上,

所有的更新都发生在主线程,而 Task 会继承 Context,因此这时候可以使用 Task.detached

1
2
3
4
5
6
7
8
9
10
Task.detached {
for i in 0...1000 {
print("Task 1: \(i)")
}
}
Task.detached {
for i in 0...1000 {
print("Task 2: \(i)")
}
}

都换成 Task.detached 之后,Task 1/2 就是一起执行了,也不会都挤在主线程上跑了。

nonisolated

相关文章:How to make parts of an actor nonisolated

还记得最开始的 actor 吧,

多个函数调用它的时候只有其中一个能执行,其余的等待上一个执行完成再进入,

这被称为隔离,很有效地避免了 Data Races

但是并非 actor 中所有的函数、属性都需要被隔离开。

  • 对 actor 扩展 Hashable 协议时,hash(into hasher: inout Hasher) 不支持 async/await

WWDC Session:利用 Swift Actor 保护可变状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
actor MyHashableActor {
let staticValue = 0
var variable = 1
}
extension MyHashableActor: Hashable {
static func == (lhs: MyHashableActor, rhs: MyHashableActor) -> Bool {
return true // Comforms to Equatable
}
nonisolated func hash(into hasher: inout Hasher) {
hasher.combine(staticValue)
hasher.combine(variable) // 🙅
// This is an error because variable is mutable
// and the `hash(into hasher: inout Hasher)` is nonisolated.
}
}
  • actor 内部的函数的其中一部分可以并发执行,充分利用多核性能。

WWDC Session: Swift 并发的可视化与优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
actor Compressor {
var state = true

// Since the function is nonisolated
// and we still need to update the state,
// so, the `compressFile()` funtion is marked `async`.
// Now, the programme will only be blocked when updating the state.
nonisolated func compressFile(_ file: File) async -> Data {
await updateState() // Blocked here to avoid Data races.
let data = compress(file) // This operation can be ran simultaneously.
await updateState() // Also blocked here to avoid Data races.
return data
}

func updateState() { state.toggle() }
}

let compressor = Compressor()
for file in files {
Task.detached {
let data = await self.compressor.compressFile(file)
}
}

func compress(_ file: File) -> Data {
// Compress file.
}

执行逻辑(个人理解)

WWDC Session:Swift 并发功能:幕后故事

  1. @MainActor 中的 Task
    • Task 中的操作均由 主线程 完成,继承 MainActor
    • 遇到 await 时, 程序被挂起,待执行的代码被迁移到其他线程(根据 priority 依次执行),主线程释放,供其他需要在主线程运行的代码继续。
    • await 结果可用之后,仍呆在原地,等待主线程空闲,之后回到主线程,恢复之前的状态继续向下执行。
  2. @MainActor 中的 Task
    • Task 中的操作均会在 其他线程 中完成,继承上层的 Context
    • 后两点与上一条基本一致
  3. Task.detached
    • 会创建一个新的线程来完成操作。
作者

LiYanan

发布于

2022-12-13

更新于

2022-12-13

许可协议

评论