本文转自 Bennyhuo 的博客
原文地址:www.bennyhuo.com/2022/02/12/…
假如我想要界说一个变量,它的值只在 Task 内部共享,怎么做到呢?
- 闲话 Swift 协程(1):Swift 协程长什么样?
- 闲话 Swift 协程(2):将回调改写成 async 函数
- 闲话 Swift 协程(3):在程序傍边调用异步函数
- 闲话 Swift 协程(4):TaskGroup 与结构化并发
- 闲话 Swift 协程(5):Task 的取消
- 闲话 Swift 协程(6):Actor 和特点隔离
- 闲话 Swift 协程(7):GlobalActor 和异步函数的调度
- 闲话 Swift 协程(8):TaskLocal
- 闲话 Swift 协程(9):异步函数与其他语言的互调用
TaskLocal 值的界说和运用
TaskLocal 值便是 Task 私有的值,不同的 Task 关于这个变量的访问将得到不同的成果。
下面咱们给出示例演示怎么界说一个 TaskLocal 值:
class Logger {
@TaskLocal
static var tag: String = "default"
}
TaskLocal 值必须界说为静态的存储特点,并运用 TaskLocal 这个特点包装器(property wrapper)来包装。TaskLocal 值也受限于特点包装器的支持规模,不能界说为尖端特点。
变量 tag 的初始值为 default
,特点包装器 TaskLocal 的构造器会接收这个值并存起来备用:
public final class TaskLocal<Value: Sendable>: Sendable, CustomStringConvertible {
let defaultValue: Value
public init(wrappedValue defaultValue: Value) {
self.defaultValue = defaultValue
}
..
}
了解特点包装器的读者应该也能想到初始值的界说还可以写:
class Logger {
@TaskLocal(wrappedValue: "default")
static var tag: String
}
经过调查 TaskLocal 的界说,咱们也发现它关于被包装的类型是有要求的,即要完成 Sendable 协议。
有关 Swift 特点包装器的介绍,可以参阅我之前的一篇文章:Kotlin 的 Property Delegate 与 Swift 的 Property Wrapper。
了解了界说之后,接下来看用法。
首先要写入值,咱们只需要调用特点包装器的 withValue 函数,它的声明如下:
final public func withValue<R>(
_ valueDuringOperation: Value,
operation: () async throws -> R,
file: String = #file,
line: UInt = #line
) async rethrows -> R
调用示例如下:
await Logger.$tag.withValue("MyTask") {
await logWithTag("in my task")
}
其中 $tag 便是 tag 的特点包装器的 projectedValue,这个值正是 TaskLocal 这个特点包装器目标自身。
public final class TaskLocal<Value: Sendable>: Sendable, CustomStringConvertible {
...
public var projectedValue: TaskLocal<Value> {
get {
self
}
set {
...
}
}
...
}
withValue 有两个参数,一个是要绑定给 tag 的值,即 MyTask
;另一个便是一个闭包,这个绑定的值只要在这个闭包傍边有效,一旦闭包执行完毕,tag 绑定的值的生命周期也就完毕了。
接下来咱们测验去读取它:
func logWithTag(_ message: Any) async {
print("(\(Logger.tag)): \(message)")
}
读取的方法就显得一般而又枯燥了。写法十分直接,不过咱们要理解,这个读的行为实际上是经过 TaskLocal 特点包装器完成的。
作为对比,咱们给出一个略微完整的比如:
await Logger.$tag.withValue("MyTask") {
await logWithTag("in withValue")
}
await logWithTag("out of withValue")
运转成果如下:
(MyTask): in withValue
(default): out of withValue
Task 关于 TaskLocal 的承继
上一篇文章傍边咱们经过示例演示了 init
和 detach
构造的 Task 实例对 actor 上下文的承继,这次咱们给咱们再演示一下对 TaskLocal 的承继,以进一步加深咱们的理解:
await Logger.$tag.withValue("MyTask") {
await Task {
await logWithTag("Task.init")
}.value
await Task.detached {
await logWithTag("Task.detached")
}.value
}
这个比如相比之前的调度器的比如就更显得一般而又枯燥了,程序输出如下:
(MyTask): Task.init
(default): Task.detached
可以看到,经过 detached
创立的 Task 实例可谓是“六亲不认”,不仅不承继 actor 的上下文,也对 TaskLocal 不管不顾。别的不难想到的是,Swift 并没有供给修正外部 TaskLocal 值的 API,因而外部的 TaskLocal 值只能被承继,不能被修正。
深入探查 TaskLocal 的存储方法
TaskLocal 值尽管看起来便是个静态存储特点,但它的值实际上是存储在 Task 相关的内存傍边的。它的读写功能天然也与它的存储方法有关,因而为了保证能够正确合理的运用 TaskLocal,咱们有必要了解一下它究竟是怎么存储的。
public final class TaskLocal<Value: Sendable>: Sendable, CustomStringConvertible {
..
// 每一个变量仅有,用于查找值的 key
var key: Builtin.RawPointer {
unsafeBitCast(self, to: Builtin.RawPointer.self)
}
// 读取 TaskLocal 值的值时调用该函数
// 经过 _taskLocalValueGet 到 Task 实例傍边查找对应的值
// 假如没有找到,则返回 defaultValue,即初始值
public func get() -> Value {
guard let rawValue = _taskLocalValueGet(key: key) else {
return self.defaultValue
}
let storagePtr =
rawValue.bindMemory(to: Value.self, capacity: 1)
return UnsafeMutablePointer<Value>(mutating: storagePtr).pointee
}
@discardableResult
public func withValue<R>(_ valueDuringOperation: Value, operation: () async throws -> R,
file: String = #file, line: UInt = #line) async rethrows -> R {
_checkIllegalTaskLocalBindingWithinWithTaskGroup(file: file, line: line)
// 写入值
_taskLocalValuePush(key: key, value: valueDuringOperation)
defer {
// 保证在 withValue 退出的时分将值释放掉
_taskLocalValuePop()
}
return try await operation()
}
...
}
这时分咱们注意到有几个关键的函数,它们的界说如下:
@_silgen_name("swift_task_localValuePush")
func _taskLocalValuePush<Value>(
key: Builtin.RawPointer/*: Key*/,
value: __owned Value
) // where Key: TaskLocal
@_silgen_name("swift_task_localValuePop")
func _taskLocalValuePop()
@_silgen_name("swift_task_localValueGet")
func _taskLocalValueGet(
key: Builtin.RawPointer/*Key*/
) -> UnsafeMutableRawPointer? // where Key: TaskLocal
经过 _silgen_name 的值,咱们可以找到他们在 C++ 傍边的界说,以 _taskLocalValueGet
为例,咱们给出 swift_task_localValueGet
的代码:
SWIFT_CC(swift)
static OpaqueValue* swift_task_localValueGetImpl(const HeapObject *key) {
if (AsyncTask *task = swift_task_getCurrent()) {
// 从当时 Task 的本地存储傍边读取值,AsyncTask 实际上便是 C++ 层傍边 Task 对应的类型
return task->localValueGet(key);
}
...
}
AsyncTask::localValueGet
本质上调用的便是 TaskLocal::Storage::getValue(AsyncTask *,const HeapObject *)
,咱们同样可以找到它的完成:
OpaqueValue* TaskLocal::Storage::getValue(AsyncTask *task,
const HeapObject *key) {
assert(key && "TaskLocal key must not be null.");
auto item = head;
// 遍历以 head 为头节点的链表
while (item) {
// 比较 key,直到找到对应的值
if (item->key == key) {
return item->getStoragePtr();
}
item = item->getNext();
}
return nullptr;
}
可见,查找进程其实便是链表的遍历查找,时间复杂度为 O(n)。
咱们再略微调查一下刺进和删除的代码:
void TaskLocal::Storage::pushValue(AsyncTask *task,
const HeapObject *key,
/* +1 */ OpaqueValue *value,
const Metadata *valueType) {
auto item = Item::createLink(task, key, valueType);
valueType->vw_initializeWithTake(item->getStoragePtr(), value);
head = item;
}
bool TaskLocal::Storage::popValue(AsyncTask *task) {
auto old = head;
head = head->getNext();
old->destroy(task);
return head != nullptr;
}
不难发现这实际上便是一个采用头插法的单链表。为什么选择这样的设计呢?
明显,绝大多数状况下 TaskLocal 值的数量都不会许多,同时刺进的值只在 withValue 函数规模内有效也使得绝大多数查找的值都排在链表前面,因而线性查找的功率并不会存在功能问题。
而链表的结构也使得增删节点十分简略,运用头插法使得 withValue 函数退出时释放销毁对应的值也变得十分简略,时间复杂度只需要 O(1)。
别的,运用单链表来存储 TaskLocal 值还有一个好处,那便是变量遮盖,例如:
await Logger.$tag.withValue("Task1") {
await logWithTag("1")
await Logger.$tag.withValue("Task2") {
await logWithTag("2")
await Logger.$tag.withValue("Task3") {
await logWithTag("3")
}
await logWithTag("22")
}
await logWithTag("11")
}
运转成果如下:
(Task1): 1
(Task2): 2
(Task3): 3
(Task2): 22
(Task1): 11
简略总结一下,TaskLocal 值是存在链表傍边的,咱们在运用进程中应当防止运用过多的 TaskLocal 值,也应该适当地减少对 TaskLocal 值的访问次数,以防止功能上最坏的状况呈现。
小结
本文咱们对 TaskLocal 值的运用和完成机制做了剖析。
关于作者
霍丙乾 bennyhuo,Kotlin 布道师,Google 认证 Kotlin 开发专家(Kotlin GDE);《深入理解 Kotlin 协程》 作者(机械工业出版社,2020.6);前腾讯高级工程师,现就职于猿辅导
- GitHub:github.com/bennyhuo
- 博客:www.bennyhuo.com
- bilibili:bennyhuo不是算命的
- 微信大众号:bennyhuo