接着上一节,我们来看看使用 TLV 时可能遇到的一些更复杂的场景。

TLV 的嵌套

首先,设置 TLV 的 withValue 是可以嵌套的,嵌套之后的规则和一般变量名的覆盖规则是相同的:内层 TLV 的值会替换掉外层 TLV。因此,下面这个例子中,内层 Work.workID 的值是 BX10,回到外层之后,就会变回 BX11:

Task(priority: .userInitiated) {
  Work.$workID.withValue("BX11") {
    syncPrintWorkID(tag: "Outer") // BX11
    
    Work.$workID.withValue("BX10") {
      syncPrintWorkID(tag: "Inner") // BX10
    }
    
    syncPrintWorkID(tag: "Outer") // BX11
  }
}

当然,在 withValue 的 closure 里也存在着一种例外的情况:

Work.$workID.withValue("BX10") {
  syncPrintWorkID(tag: "Inner") // BX10
  
  Task.detached(priority: .userInitiated) {
    syncPrintWorkID(tag: "Detached")
  }
}

之前我们说过,detached task 并不会继承创建它的时候的上下文,因此,它当然也就不会继承属于这个上下文里的 TLV。尽管我们通过 withValueWork.workID 设置成了 BX10,在 detached task 中的 Work.workID 的值仍旧是 no-work-id。

TLV 在函数调用过程中的可见性

第二个场景,TLV 的值是会随着函数调用栈一直传递下去的:

func inner() -> String? {
  syncPrintWorkID(tag: "inner") // BX11
  return Work.workID
}

func middle() async -> String? {
  syncPrintWorkID(tag: "middle") // BX11
  return inner()
}

func outer() async -> String? {
  await Work.$workID.withValue("BX11") {
    syncPrintWorkID(tag: "outer")
    let ret = await middle()
    
    return ret
  }
}

例如上面这个例子,我们在 outer 中把 Work.workID 设置成 BX11。在接下来的调用栈里,middleinner 中读取 Work.workID 的值得到的都是 BX11。因此,当我们看到直接读取 TLV 的时候,并不一定读到的就是默认值。

TLV 在子任务中的继承

第三个场景,就是子任务继承父任务 TLV 的情况了。当我们在一个任务上下文环境中使用 withValue 设置了 TLV,在 withValue 的 closure 中继续创建子任务的时候,子任务中读到的 Work.workID 也会是 BX11。也就是说,父子任务的 TLV 不是独立的,它会继承到子任务。当然,子任务也可以在它自己的作用域里,覆盖掉父任务的值。

Work.$workID.withValue("BX11") {
  Task(priority: .userInitiated) {
    syncPrintWorkID(tag: "SubTask")
  }
}

这个例子应用到异步版本的 withValue 中,也同样适用。通过 group.addTask 创建的任务中,Work.workID 的值也是 BX11:

await Work.$workID.withValue("BX11") {
  await withTaskGroup(of: String?.self) { group -> String? in
    group.addTask {
      syncPrintWorkID(tag: "SubTask")
      return Work.workID
    }
    
    return await group.next()!
  }
}

但要注意的是,我们只能在 withValue 的 closure 中创建任务群组。如果在 withTaskGroup 中设置 TLV,则会触发运行时错误。在当前的 Swift 5.5 版本里,这是一个不被支持的编程方式:

await withTaskGroup(of: Void.self) { group in
  Work.$workID.withValue("BX11") { // Runtime Exception
    syncPrintWorkID(tag: "Do not do this")

    group.addTask {}
  }
}

没有任务上下文环境的情况

说到这里,我们所有的例子,都是带有任务上下文环境的。如果我们在没有任务上下文环境的情况下访问或设置 TLV 会如何呢:

@main
struct MyApp {
  static func main() {
    syncPrintWorkID(tag: "Outer") // BX11

    Work.$workID.withValue("BX10") {
      syncPrintWorkID(tag: "Inner") // BX10
    }

    syncPrintWorkID(tag: "Outer") // BX11
  }
}

在上面这个例子中,Swift 会借用线程局部存储来模拟 TLV 需要的上下文环境。我们刚才说过的作用域以及 API 的用法,在传统的同步代码环境下,行为和之前是一一样的。

What's next?

至此,关于 TLV 的基础内容,我们就说的差不多了。下一节,我们来说一个相比 TLV 更为常用的 Swift 5.5 新特性:async let

所有订阅均支持 12 期免息分期

¥ 59

按月订阅

一个月,观看并下载所有视频内容。初来泊学,这可能是个最好的开始。

开始订阅

¥ 512

按年订阅

一年的时间,让我们一起疯狂地狩猎知识吧。比按月订阅优惠 28%

开始订阅

¥ 1280

泊学终身会员

永久观看和下载所有泊学网站视频,并赠送 100 元商店优惠券。

我要加入
如需帮助,欢迎通过以下方式联系我们