普通视图
如何提高前端应用的性能?
Swift 6 新特性(一):count(where:) 方法带来的从复杂到简洁变化
为什么那些看起很丑的产品却能上架AppStore?
swift的get和set,newValue和oldValue
Xcode 14.3 和 iOS 16.4 为 SwiftUI 带来了哪些新功能?
有用的知识又增加了:为何无法编译某些 WWDC 官方视频中的代码?
iOS 17(SwiftUI 5.0)带来的图表(Charts)新类型:“大饼”与“甜甜圈”
UINavigationController 导航控制器
Xcode 高效秘诀:这 11 个快捷键你必须知道!
包瘦身之未引用图片资源扫描工具
Swift 协议之 Equatable
iOS26适配指南之Update Properties
一行命令生成xcode自定义模板工程
日月之行,若出其中。星汉灿烂,若出其里。
SwiftUI 5.0(iOS 17)TipKit 让用户更懂你的 App
Swift 新并发模型中 isolated 和 nonisolated 关键字的含义看这篇就懂了!
概览
在 Swift 新 async/await 并发模型中,我们可以利用 Actor 来避免并发同步时的数据竞争,并从语义上简化代码。
Actor 伴随着两个独特关键字:isolated
和 nonisolated
,弄懂它们的含义、合理合规的使用它们是完美实现同步的必要条件。
那么小伙伴们真的搞清楚它们了吗?
在本篇博文中,您将学到如下内容:
- isolated 关键字
- nonisolated 关键字
- 没有被 async 修饰的方法也可以被异步等待!
闲言少叙,让我们即刻启航!
Let‘s go!!!;)
isolated 关键字
Actor 从本质上来说就是一个同步器
,它必须严格限制单个实例执行上下文以满足同步的语义。
这意味着在 Actor 中,所有可变属性、计算属性以及方法等默认都是被隔离执行的。
actor Foo {
let name: String
let age: Int
var luck = 0
init(name: String, age: Int, luck: Int = 0) {
self.name = name
self.age = age
self.luck = luck
}
func incLuck() {
luck += 1
}
var fullDesc: String {
"\(name)[\(age)] luck is *\(luck)*"
}
}
如上代码所示,Foo 中的 luck 可变属性、incLuck 方法以及 fullDesc 计算属性默认都被打上了 isolated 烙印。大家可以想象它们前面都隐式被 isolated 关键字修饰着,但这不能写出来,如果写出来就会报错:
在实际访问或调用这些属性或方法时,必须使用 await 关键字:
Task {
let foo = Foo(name: "hopy", age: 11)
await foo.incLuck()
print(await foo.luck)
print(await foo.fullDesc)
}
正是 await 关键字为 Foo 实例内容的同步创造了隔离条件,以摧枯拉朽之势将数据竞争覆巢毁卵。
nonisolated 关键字
但是在有些情况下 isolated 未免有些“防御过度”了。
比如,如果我们希望 Foo 支持 CustomStringConvertible 协议,那么势必需要实现 description 属性:
extension Foo: CustomStringConvertible {
var description: String {
"\(name)[\(age)]"
}
}
如果大家像上面这样写,那将会妥妥的报错:
因为 description 作为计算属性放在 Actor 中,其本身默认处在“隔离”状态,而 CustomStringConvertible 对应的 description 实现必须是“非隔离”状态!
大家可以这样理解:我们不能异步调用 foo.description!
extension Foo: CustomStringConvertible {
/*
var description: String {
"\(name)[\(age)]"
}*/
var fakeDescription: String {
"\(name)[\(age)]"
}
}
Task {
let foo = Foo(name: "hopy", age: 11)
// foo.description 不能异步执行!!!
print(await foo.fakeDescription)
}
大家或许注意到,在 Foo#description 中,我们只使用了 Foo 中的只读属性。因为 Actor 中只读属性都是 nonisolated 隐式修饰,所以这时我们可以显式用 nonisolated 关键字修饰 description 属性,向 Swift 表明无需考虑 Foo#description 计算属性内部的同步问题,因为里面没有任何可变的内容:
extension Foo: CustomStringConvertible {
nonisolated var description: String {
"\(name)[\(age)]"
}
}
Task {
let foo = Foo(name: "hopy", age: 11)
print(foo)
}
但是,如果 nonisolated 修饰的计算属性中含有可变(isolated)内容,还是会让编译器“怨声载道”:
没有被 async 修饰的方法也可以被异步等待!
最后,我们再介绍 isolated 关键字一个非常有用的使用场景。
考虑下面的 incLuck() 全局函数,它负责递增传入 Foo 实例的 luck 值,由于 Actor 同步保护“魔法”的存在,它必须是一个异步函数:
func incLuck(_ foo: Foo) async {
await foo.incLuck()
}
不过,如果我们能够保证 incLuck() 方法传入 Foo 实参的“隔离性”,则可以直接访问其内部的“隔离”(可变)属性!
如何保证呢?
很简单,使用 isolated 关键字:
func incLuck2(_ foo: isolated Foo) {
foo.luck += 1
}
看到了吗? luck 是 Foo 内部的“隔离”属性,但我们竟然可以在外部对其进行修改,是不是很神奇呢?
这里,虽然 incLuck2() 未用 async 修饰,但它仍是一个异步方法,我称之为全局“隐式异步”方法:
Task {
let foo = Foo(name: "hopy", age: 11)
await incLuck(foo)
await incLuck2(foo)
}
虽然 foo 是一个 Actor 实例,它包含一些外部无法直接查看的“隔离”内容,但我们仍然可以使用一些调试手段探查其内部,比如 dump 方法:
Task {
let foo = Foo(name: "hopy", age: 11)
await incLuck(foo)
await incLuck2(foo)
dump(foo)
}
输出如下:
over
hopy[11]
▿ hopy[11] #0
- $defaultActor: (Opaque Value)
- name: "hopy"
- age: 11
- luck: 2
通过 dump() 方法输出可以看到,foo 的 luck 值被正确增加了 2 次,棒棒哒!!!💯
总结
在本篇博文中,我们通过几个通俗易懂的例子让小伙伴们轻松了解到 Swift 新 async/await 并发模型中 isolated 与 nonisolated 关键字的精髓,并对它们做了进一步的深入拓展。
感谢观赏,再会!8-)