如果之前你从事过多线程方面的开发,一定对线程局部存储(TLS)这个东西并不陌生。这种类型的对象把线程当作自己的容器,在线程开始时创建,线程结束时回收。这种技术被广泛地应用在多线程环境下的调试以及性能分析工具的开发中。
但由于 Swift 5.5 的并发工作模型并不依赖操作系统的线程管理,TLS 并不适用于 Swift 中的多任务环境,这就给和结构化并发模型有关的调试和分析带来了一定的麻烦。为了解决这个问题,Swift 提出了一个新的概念,叫做 Task Local Value(以下我们就简称 TLV 了),它可以让我们把值绑定到单独的任务上。
其实,如果回顾下之前我们提到过的内容就会发现,Swift 中的 Task
已经具备了“携带私有数据”的能力。例如:访问任务优先级的 Task.priority
以及查询取消状态的 Task.isCancelled
。但由于它们是属于每个任务都必须具备的信息,Swift 在实现它们的时候,采取了一些优化手段。因此,虽然形式上类似,但TLV 并不能和这些数据保存在同一个地方。为此,Swift 提供了一个 property wrapper 以及一个 API,分别来定义和设置 TLV 的值。
定义 TLV
为了定义一个 TVL 我们可以使用 @TaskLocal
:
struct Work {
@TaskLocal
static var workID: String?
}
在 Swift 5.5 里,@TaskLocal
只能用于修饰某个静态存储属性,我们暂时还不能把一个全局变量修饰为 TLV。另外,自定义的 TLV 通常都是一个 optional 类型,并且默认值为 nil
,因为不一定每个任务都需要设置和使用这个值。但这也有例外,如果把 Task.priority
也看成是一个 TLV 的话,它就有一个非 nil
的默认值 .unspecified
。
访问和设置 TLV
定义好 TLV 之后,无论是同步还是异步环境里,也无论是有无任务的上下文环境,我们都可以访问它,但访问的行为,又有些许不同。为了演示这些例子,我们定义两个全局函数,为了稍后方便观察,我们让这两个函数接受一个用于标记结果的参数 tag
。它们的功能,就是读取 TLV 并打印在控制台上:
func asyncPrintWorkID(tag: String) async {
print("\(tag) \(Work.workID ?? "no-work-id")")
}
func syncPrintWorkID(tag: String) {
print("\(tag) \(Work.workID ?? "no-work-id")")
}
然后,来看下面的代码:
await withTaskGroup(of: Void.self) { group in
group.addTask {
syncPrintWorkID(tag: "T1") // no-work-id
await asyncPrintWorkID(tag: "T1") // no-work-id
}
group.addTask {
syncPrintWorkID(tag: "T2") // no-work-id
await asyncPrintWorkID(tag: "T2") // no-work-id
}
}
我们创建了两个不同的任务上下文环境,在里面分别用同步和异步方法访问了 Work.workID
,执行一下,应该可以看到下面这样的结果:
T1 no-work-id
T2 no-work-id
T1 no-work-id
T2 no-work-id
这时你可能会想,就不能在每个任务里先设置好 Work.workID
之后再调用这些方法么?当然可以,但又不完全可以。因为就像刚才说过的,TLV 的设置,是通过一个叫做 withValue
的 API 完成的,我们不能像给一个普通变量赋值一样的去设置 TLV。来看下面这个例子:
// Example 2
await withTaskGroup(of: Void.self) { group in
group.addTask {
syncPrintWorkID(tag: "T1") // no-work-id
await Work.$workID.withValue("BX11") {
syncPrintWorkID(tag: "T1") // BX11
await asyncPrintWorkID(tag: "T1") // BX11
}
await asyncPrintWorkID(tag: "T1") // no-work-id
}
group.addTask {
syncPrintWorkID(tag: "T2") // no-work-id
await Work.$workID.withValue("BX10") {
syncPrintWorkID(tag: "T2") // BX10
await asyncPrintWorkID(tag: "T2") // BX10
}
await asyncPrintWorkID(tag: "T2") // no-work-id
}
}
这次,我们得到的结果是这样的:
T1 no-work-id
T2 no-work-id
T1 BX11
T1 BX11
T1 no-work-id
T2 BX10
T2 BX10
T2 no-work-id
可以看到,同样的 Work.workID
,在两个不同的任务里,它们分别变成了 BX11 和 BX10。只是,为了访问 withValue
,我们要使用 $workID
得到对应的 property wrapper 类型(在这个例子中,也就是 TaskLocal<String?>
)。然后再访问它的 withValue
方法。withValue
的第一个参数表示要绑定到当前任务的值,第二个参数,是一个 closure,只有在这个 closure 里,workID
的值才是我们绑定的参数值。离开 closure 之后,就又回到了 Example 1 的情况,Work.workID
的值又会恢复成 nil
。
另外,withValue
也有同步和异步的版本,如果对 TLV 的操作不包含异步方法调用,我们也可以使用同步的版本:
Work.$workID.withValue("BX10") {
syncPrintWorkID(tag: "T2") // BX10
}
What's next?
以上,就是关于 TLV 最基本的概念和用法。下一节,我们来看看在实际应用中可能遇到的一些更复杂的场景,例如:TLV 的嵌套、TLV 在多层函数调用下的可见性以及没有任务上下文环境下的 TLV。