
楔子
江湖风云变幻,Swift 武林近日再掀波澜。
传闻 Apple 于密室推演三月,终得《Swift 6.2 并发新篇》,扬言要破解困扰开发者多年的 "经脉错乱" 之症 ------ 那便是异步函数与同步函数运行规则不一、主 Actor 调用常生冲突之陈年旧疾。

想当年,多少英雄好汉折戟于 GCD 到 Swift 并发的转型之路:明明是同门函数,同步者循调用者经脉而行,异步者却偏要另辟蹊径,轻则编译器怒目相向,重则数据走火入魔。
如今 6.2 版本携 "nonisolated (nonsending) " 与 "defaultIsolation" 两大神功而来,声称要让代码运行如行云流水,再无经脉冲突之虞。
在本篇博文,您将学到如下内容:
- @concurrent:破界而行的旁门秘籍
- 何时启用 NonisolatedNonsendingByDefault?非必要不增乱
- defaultIsolation:主 Actor 门派的包级新规矩
- defaultIsolation 实战:隐式标注,逆势需明
- 逆势而行:如何脱离主 Actor?
- 该不该启用 defaultIsolation?利弊权衡,因地制宜
今日列位武林好汉且随老夫执剑开卷,一探这两门新功究竟有何玄妙,又将如何改写 Swift 并发的江湖格局吧。
Let's go!!!;)
6. @concurrent:破界而行的旁门秘籍
与 "nonisolated (nonsending) " 一同出现的,还有 "@concurrent" 这枚破界令牌。
有了这枚令牌,函数的行为就能和 Swift 6.1 旧制一样 ------ 脱离调用者的经脉,建立新的隔离语境(专属经脉)。
请看示例:
swift
@MainActor
class NetworkingClient {
@concurrent
nonisolated func loadUserPhotos() async throws -> [Photo] {
return [Photo()]
}
}
给函数标注@concurrent
,就如同立下誓言:必定脱离调用者的 Actor,创建专属的隔离语境。
这枚令牌有一条铁律:只能用于 "nonisolated" 函数。如果将其用于 Actor 门派的招式,除非该招式明确标注 "nonisolated",否则就是违规练功:
swift
actor SomeGenerator {
// 此为禁忌,不可如此
@concurrent
func randomID() async throws -> UUID {
return UUID()
}
// 此为正道,可如此行
@concurrent
nonisolated func randomID() async throws -> UUID {
return UUID()
}
}
需要注意的是,在撰写本文时,这两种写法暂时都能运行,而且未标注 "nonisolated" 的@concurrent
函数在运行时似乎没有隔离 ------ 但根据《SE-0461 秘籍》的规定,这是 Swift 6.2 工具链的一个临时疏漏,日后定会修正,大家千万不要效仿前者,以免走火入魔。

7. 何时启用 NonisolatedNonsendingByDefault?非必要不增乱
在老夫看来,启用 "NonisolatedNonsendingByDefault" 令牌实乃明智之举。
它带来了一种新的工作方式:nonisolated 异步函数会跟随调用者的 Actor 运行,而非在独立的隔离语境中运行。实际上,这能减少很多编译器错误,而且根据本人的尝试,还能省去不少主 Actor 标注。
老夫一直主张减少应用中的并发量,只在确实需要时才引入 ------ 这一令牌恰好能帮助宝子们实现这一点。在决定给应用中的所有内容都标注@concurrent
以确保安全之前,请先问问自己是否真的有此必要。
其实,很可能根本没必要,而且从整体来看,并非所有代码都并发运行,这会让代码及其执行过程更容易理解。
当同时采用 Swift 6.2 的第二项主要特性 "defaultIsolation" 时,情况更是如此。
8. defaultIsolation:主 Actor 门派的包级新规矩
Swift 6.2 的另一大绝招 "defaultIsolation",堪称主 Actor 门派的包级(package)新规。
在 Swift 6.1 中,代码只有在被@MainActor
标注(或遵循标有@MainActor
的协议)时,才会归入主 Actor------ 就像只有加入主 Actor 门派,才能学习主经脉的功法一样。

给代码标注@MainActor
是解决编译器错误的常用方法,而且大多数情况下都是正确的做法。并非所有代码都需要在后台线程异步执行。那样做成本相对较高,往往对性能没有提升,还会让代码难以理解。
在采用 Swift 并发之前,各位秃头少侠们不会到处使用,那样做成本相对较高,往往对性能没有提升,反而还会让代码难以理解。
在采用 Swift 并发之前,小伙伴们不会到处使用DispatchQueue.global()
,那现在又何必做类似的事情呢?
言归正传,在 Swift 6.2 中,我们可以在包级别(package)设置默认在主 Actor 上运行代码。这是《SE-0466 秘籍》引入的特性。
这意味着,UI 包、应用目标和模型包等,都能自动在主 Actor 上运行代码,除非通过@concurrent
或自定义 Actor 明确选择不在主 Actor 上运行。
要启用这一特性,可在swiftSettings
中设置defaultIsolation
,或作为编译器参数传入:
swift
swiftSettings: [
.defaultIsolation(MainActor.self),
.enableExperimentalFeature("NonisolatedNonsendingByDefault")
]
不必将defaultIsolation
与NonisolatedNonsendingByDefault
一起使用,但在老夫的实验中,同时使用这两个选项效果很好。

9. defaultIsolation 实战:隐式标注,逆势需明
目前,可以将MainActor.self
作为默认隔离传入,使所有代码默认在主 Actor 上运行;也可以传入nil
以保持现有行为(或者根本不传入该设置,同样保持现有行为)。

启用这一特性后,Swift 会推断所有对象都带有@MainActor
标注,除非明确指定其他情况:
swift
@Observable
class Person {
var myValue: Int = 0
let obj = TestClass()
// 如果 defaultIsolation 设置为主 Actor,此函数始终在主 Actor 上运行
func runMeSomewhere() async {
MainActor.assertIsolated()
// 执行一些操作、调用异步函数等
}
}
这段代码包含一个 nonisolated 异步函数。这意味着,默认情况下,它会继承调用runMeSomewhere
的 Actor。如果从主 Actor 调用,它就在主 Actor 上运行;如果从另一个 Actor 或非 Actor 处调用,它就不在主 Actor 上运行。
这很可能完全不是预期的结果。
或许我们编写异步函数只是为了能调用其他需要等待的函数。如果runMeSomewhere
不执行任何繁重的处理,我们可能希望Person
在主 Actor 上。它是一个可观察类,很可能用于驱动 UI,这意味着几乎所有对该对象的访问都应该在主 Actor 上进行。
当defaultIsolation
设置为MainActor.self
时,Person
会被隐式标注@MainActor
,因此Person
的所有工作都在主 Actor 上运行。
10. 逆势而行:如何脱离主 Actor?
假设我们想给Person
添加一个不在主 Actor 上运行的函数。可以像往常一样使用 nonisolated:
swift
// 此函数将在调用者的 Actor 上运行
nonisolated func runMeSomewhere() async {
MainActor.assertIsolated()
// 执行一些操作、调用异步函数等
}
如果想确保函数绝对不在主 Actor 上运行则可以这样做:
swift
// 此函数将在调用者的 Actor 上运行
@concurrent
nonisolated func runMeSomewhere() async {
MainActor.assertIsolated()
// 执行一些操作、调用异步函数等
}
对于每个想要设置为 nonisolated 的函数或属性,都需要明确选择不使用主 Actor 推断;不能为整个类型进行这样的设置。
当然,自定义的 Actor 不会突然开始在主 Actor 上运行,而且标注了其它全局 Actor 的类型也不会受到这一变化的影响。
11. 该不该启用 defaultIsolation?利弊权衡,因地制宜
这个问题很难回答。老夫的初步想法是 "应该启用"。
对于应用目标、UI 包和主要存放视图模型的包,默认在主 Actor 上运行无疑是正确的选择。

不过,列位仍然可以在需要的地方引入并发,而且会比以往更具目的性。
那么本篇的内容就要告一段落了!大家学"废"了吗?
最后,感谢大家的观看,再会啦!8-)