概览
在 Swift 新 async/await 并发模型中,我们可以利用 Actor 来避免并发同步时的数据竞争,并从语义上简化代码。
Actor 伴随着两个独特关键字:isolated
和 nonisolated
,弄懂它们的含义、合理合规的使用它们是完美实现同步的必要条件。
那么小伙伴们真的搞清楚它们了吗?
在本篇博文中,您将学到如下内容:
文章目录
- 概览
- isolated 关键字
- nonisolated 关键字
- 没有被 async 修饰的方法也可以被异步等待!
- 总结
闲言少叙,让我们即刻启航!
Let‘s go!!!😉
isolated 关键字
Actor 从本质上来说就是一个同步器
,它必须严格限制单个实例执行上下文以满足同步的语义。
这意味着在 Actor 中,所有可变属性、计算属性以及方法等默认都是被隔离执行的。
actor Foo {let name: Stringlet age: Intvar luck = 0init(name: String, age: Int, luck: Int = 0) {self.name = nameself.age = ageself.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 关键字的精髓,并对它们做了进一步的深入拓展。
感谢观赏,再会!😎