2025-02-08 16:39:33
,某些文章具有时效性,若有错误或已失效,请在下方留言。Swift lets us attach metadata to a task using task-local values, which are small pieces of information that any code inside a task can read.
Swift 允许我们使用 task-local 值将元数据附加到 task,这些值是 task 中的任何代码都可以读取的小块信息。
For example, we can read Task.isCancelled
to see whether the current task is cancelled or not, but that’s not a true static property – it’s scoped to the current task, rather than shared across all tasks. This is the power of task-local values: the ability to create static-like properties inside a task.
例如,我们可以读取 Task.isCancelled
来查看当前任务是否被取消,但这不是真正的静态属性 – 它的范围限定为当前任务,而不是在所有任务之间共享。这就是 task-local 值的强大之处:能够在 task 中创建类似 static 的属性。
Important: Most people will not want to use task-local values – if you’re just curious you’re welcome to read on and explore how task-local values work, but honestly they are useful in only a handful of very specific circumstances and if you find them complex I wouldn’t worry too much.
重要: 大多数人不会想使用 task-local 值 – 如果你只是好奇,欢迎你继续阅读并探索 task-local 值是如何工作的,但老实说,它们只在少数非常特殊的情况下有用,如果你觉得它们很复杂,我不会太担心。
Task-local values are analogous to thread-local values in an old-style multithreading environment: we attach some metadata to our task, and any code running inside that task can read that data as needed.
任务本地值类似于老式多线程环境中的线程本地值:我们将一些元数据附加到任务中,在该任务中运行的任何代码都可以根据需要读取该数据。
Swift’s implementation is carefully scoped so that you create contexts where the data is available, rather than just injecting it directly into the task, which makes it possible to adjust your metadata over time. However, inside that context all code is able to read your task-local values, regardless of how it’s used.
Swift 的实现范围经过仔细设计,因此您可以创建数据可用的上下文,而不仅仅是将其直接注入到任务中,这使得随着时间的推移调整元数据成为可能。但是,在该上下文中,所有代码都能够读取 task-local 值,无论其使用方式如何。
Using task-local values happens in three steps:
使用 task-local 值分为三个步骤:
- Creating a type that has one or more properties we want to make into task-local values. This can be an enum, struct, class, or even actor if you want, but I’d suggest starting with an enum so it’s clear you don’t intend to make instances of the type.
创建一个类型,该类型具有一个或多个我们想要转换为 task-local 值的属性。如果你愿意,这可以是 enum、struct、class 甚至 actor,但我建议从 enum 开始,这样很明显你不打算创建这种类型的实例。 - Marking each of your task-local values with the
@TaskLocal
property wrapper. These properties can be any type you want, including optionals, but must be marked asstatic
.
使用@TaskLocal
属性包装器标记每个任务本地值。这些属性可以是您想要的任何类型,包括可选类型,但必须标记为static
。 - Starting a new task-local scope using
YourType.$yourProperty.withValue(someValue) { … }
.
使用YourType.$yourProperty.withValue(someValue) { … }
启动新的任务本地范围 。
Inside the task-local scope, any time you read YourType.yourProperty
you will receive the task-local value for that property – it’s not a regular static property that has a single value shared between all parts of your program, but instead it can return a different value depending on which task tries to read it.
在 task-local 范围内,每当读取 YourType.yourProperty
时,您都会收到该属性的 task-local 值 – 它不是一个常规的静态属性,在程序的所有部分之间共享一个值,而是可以返回一个不同的值,具体取决于哪个任务尝试读取它。
To demonstrate task-local values in action, I want to give you two examples: the first is a simple toy example that demonstrates the code required to use them and how they work, but the second is a more real-world example that’s actually useful.
为了演示 task-local 值的实际作,我想给您提供两个示例:第一个是一个简单的玩具示例,演示了使用它们所需的代码以及它们的工作原理,但第二个是实际有用的更实际的示例。
First, our simple example. This will create a User
enum with a id
property that is marked @TaskLocal
, then it will launch a couple of tasks with different values for that user ID. Each task will do exactly the same thing: print the user ID, sleep for a small amount of time, then print the user ID again, which will allow you to see both tasks running at the same time while having their own unique task-local user ID.
首先,我们的简单示例。这将创建一个具有标记为 @TaskLocal
的 id
属性的 User
枚举,然后它将为该用户 ID 启动几个具有不同值的任务。每个任务将执行完全相同的作:打印用户 ID,休眠一小段时间,然后再次打印用户 ID,这将允许您看到两个任务同时运行,同时拥有自己唯一的任务本地用户 ID。
Here’s the code: 这是代码:
enum User {
@TaskLocal static var id = "Anonymous"
}
@main
struct App {
static func main() async throws {
let first = Task {
try await User.$id.withValue("Piper") {
print("Start of task: \(User.id)")
try await Task.sleep(for: .seconds(1))
print("End of task: \(User.id)")
}
}
let second = Task {
try await User.$id.withValue("Alex") {
print("Start of task: \(User.id)")
try await Task.sleep(for: .seconds(1))
print("End of task: \(User.id)")
}
}
print("Outside of tasks: \(User.id)")
try await first.value
try await second.value
}
}
When that code runs it will print:
当该代码运行时,它将打印:
- Outside of tasks: Anonymous
任务之外:匿名 - Start of task: Piper 任务开始时间:Piper
- Start of task: Alex 任务开始时间: Alex
- End of task: Piper 任务结束:Piper
- End of task: Alex 任务结束:Alex
Of course, because the two tasks run independently of each other you might also find that the order of Piper and Alex switch. The important thing is that each task has its own value for User.id
even as they overlap, and code outside the task will continue to use the original value.
当然,由于这两个任务彼此独立运行,您可能还会发现 Piper 和 Alex 的顺序发生了变化。重要的是,每个任务都有自己的 User.id
值,即使它们重叠也是如此,并且任务外部的代码将继续使用原始值。
As you can see, Swift makes it impossible to forget about a task-local value you’ve set, because it only exists for the work inside withValue()
. This scoping approach also means it’s possible to nest multiple task locals as needed, and you can even shadow task locals – start a scope for one, do some work, then start another nested scope for that same property. so that it temporarily has a different value.
正如你所看到的,Swift 让你不可能忘记你设置的任务本地值,因为它只存在于 withValue()
中的工作中。这种范围界定方法还意味着可以根据需要嵌套多个任务局部变量,你甚至可以影子任务局部变量 —— 为一个任务局部变量启动一个范围,做一些工作,然后为同一属性启动另一个嵌套范围。,以便它暂时具有不同的值。
In real-world code, task-local values are useful for places where you need to repeatedly pass values around inside your tasks – values that need to be shared within the task, but not across your whole program like a singleton might be.
在实际代码中,任务本地值对于需要在任务内部重复传递值的地方很有用 – 这些值需要在任务中共享,但不能像单例那样在整个程序中共享。
For example, the Swift Evolution proposal for task-local values (https://github.com/apple/swift-evolution/blob/main/proposals/0311-task-locals.md) suggests examples such as tracing, mocking, progress monitoring, and more.
例如,针对任务本地值 (https://github.com/apple/swift-evolution/blob/main/proposals/0311-task-locals.md) 的 Swift Evolution 提案提出了跟踪、模拟、进度监控等示例。
As a more complex example, we could create a simple Logger
struct that writes out messages depending on the current level of logging: debug being the lowest log level, then info, warn, error, and finally fatal at the highest level.
作为一个更复杂的示例,我们可以创建一个简单的 Logger
结构体,该结构体根据当前的日志记录级别写出消息:debug 是最低的日志级别,然后是 info、warn、error,最后是最高级别的 fatal。
If we make the log level – which messages to print – be a task-local value, then each of our tasks can have whatever level of logging they want, regardless of what other tasks are doing.
如果我们将日志级别 – 要打印的消息 – 设为 task-local 值,那么我们的每个任务都可以拥有它们想要的任何级别的日志记录,而不管其他任务正在做什么。
To make this work we need three things:
要做到这一点,我们需要三件事:
- An enum to describe the five levels of logging.
一个枚举,用于描述日志记录的五个级别。 - A
Logger
struct that is a singleton.
一个Logger
结构,它是一个单例。 - A task-local property inside
Logger
to store the current log level. (Even though the logger is a singleton, the log level is task-local.)Logger
中的任务本地属性,用于存储当前日志级别。(即使 Logger 是单例的,日志级别也是任务本地的。
On top of that, we need a couple more things to actually demonstrate the logger in action: a fetch()
method that downloads data from a URL and creates various logging messages, and a couple of tasks that call fetch()
with different task-local log settings so we can see exactly how it all works.
最重要的是,我们还需要另外一些东西来实际演示 Logger 的作:一个从 URL 下载数据并创建各种日志记录消息的 fetch()
方法,以及几个使用不同的任务本地日志设置调用 fetch()
的任务,以便我们可以确切地看到它是如何工作的。
Here’s the code: 这是代码:
// Our five log levels, marked Comparable so we can use < and > with them.
enum LogLevel: Comparable {
case debug, info, warn, error, fatal
}
struct Logger {
// The log level for an individual task
@TaskLocal static var logLevel = LogLevel.info
// Make this struct a singleton
private init() { }
static let shared = Logger()
// Print out a message only if it meets or exceeds our log level.
func write(_ message: String, level: LogLevel) {
if level >= Logger.logLevel {
print(message)
}
}
}
@main
struct App {
// Returns data from a URL, writing log messages along the way.
static func fetch(url urlString: String) async throws -> String? {
Logger.shared.write("Preparing request: \(urlString)", level: .debug)
if let url = URL(string: urlString) {
let (data, _) = try await URLSession.shared.data(from: url)
Logger.shared.write("Received \(data.count) bytes", level: .info)
return String(decoding: data, as: UTF8.self)
} else {
Logger.shared.write("URL \(urlString) is invalid", level: .error)
return nil
}
}
// Starts a couple of fire-and-forget tasks with different log levels.
static func main() async throws {
let first = Task {
try await Logger.$logLevel.withValue(.debug) {
try await fetch(url: "https://hws.dev/news-1.json")
}
}
let second = Task {
try await Logger.$logLevel.withValue(.error) {
try await fetch(url: "")
}
}
_ = try await first.value
_ = try await second.value
}
}
When that runs you’ll see “Preparing request: https://hws.dev/news-1.json” as the first task starts, then “URL is invalid” as the second task starts (I used an empty string rather than a real URL), then “Received 8075 bytes” as the first task finishes downloading its data.
当该任务运行时,您将在第一个任务开始时看到“Preparing request: https://hws.dev/news-1.json”,然后在第二个任务开始时看到“URL is invalid”(我使用的是空字符串而不是真实的 URL),然后在第一个任务完成下载其数据时看到“Received 8075 bytes”。
So, here our fetch()
method doesn’t even need to know that a task-local value is being used – it just calls the Logger
singleton, which in turn refers to the task-local value.
因此,这里的 fetch()
方法甚至不需要知道正在使用任务本地值 – 它只需调用 Logger 单例,而 Logger
单例又引用任务本地值。
To finish up, I want to leave you with a few important tips for using task-local values:
最后,我想给您留下一些关于使用 task-local 值的重要提示:
- It’s okay to access a task-local value outside of a
withValue()
scope – you’ll just get back whatever default value you gave it.
在withValue()
范围之外访问 task-local 值是可以的 —— 你只会得到你给它的任何默认值。 - Although regular tasks inherit task-local values of their parent task, detached tasks do not because they don’t have a parent.
尽管常规任务继承其父任务的任务本地值,但分离任务不会继承,因为它们没有父任务。 - Task-local values are read-only; you can only modify them by calling
withValue()
as shown above.
任务本地值是只读的;你只能通过调用withValue()
来修改它们,如上所示。
And finally, one important quote from the Swift Evolution proposal for this feature: “please be careful with the use of task-locals and don’t use them in places where plain-old parameter passing would have done the job.” Put more plainly, if task locals are the answer, there’s a very good chance you’re asking the wrong question.
最后,Swift Evolution 提案中对此功能的一句重要引用:“请小心使用 task-locals,不要在普通参数传递可以完成工作的地方使用它们。更简单地说,如果 task locals 是答案,那么您很有可能问错了问题。