2025-02-08 11:58:59
,某些文章具有时效性,若有错误或已失效,请在下方留言。If you create a new task using the regular Task
initializer, your work starts running immediately and inherits the priority of the caller, any task local values, and its actor context. On the other hand, detached tasks also start work immediately, but do not inherit the priority or other information from the caller.
如果您使用常规 Task
初始值设定项创建新任务,则您的工作将立即开始运行,并继承调用方的优先级、任何任务本地值及其参与者上下文。另一方面,分离的任务也会立即开始工作,但不会从调用方继承优先级或其他信息。
I’m going to explain in more detail why these differences matter, but first I want to mention this very important quote from the Swift Evolution proposal for async let
: “Task.detached
most of the time should not be used at all.” I’m getting that out of the way up front so you don’t spend time learning about detached tasks, only to realize you probably shouldn’t use them!
我将更详细地解释为什么这些差异很重要,但首先我想提一下 Swift Evolution 对 async let
的提案中非常重要的引用:“Task.detached
大多数时候根本不应该使用。我提前把这个问题说清楚了,这样你就不会花时间学习分离的任务,只是意识到你可能不应该使用它们!
Still here? Okay, let’s dig in to our three differences: priority, task local values, and actor isolation.
还在这里?好了,让我们深入研究一下我们的三个区别:优先级、任务本地值和 actor 隔离。
The priority part is straightforward: if you’re inside a high-priority task and create a new task, it will also have a high priority, whereas creating a new detached task would have a nil priority unless you specifically asked for something.
优先级部分很简单:如果你在一个高优先级任务中并创建了一个新任务,它也将具有高优先级,而创建一个新的分离任务将具有 nil 优先级,除非你特别要求某些内容。
The task local values part is a little more complex, but to be honest probably isn’t going to be of interest to most people. Task local values allow us to share a specific value everywhere inside one specific task – they are like static properties on a type, except rather than everything sharing that property, each task has its own value. Detached tasks do not inherit the task local values of their parent because they do not have a parent.
任务 local values 部分稍微复杂一些,但老实说,大多数人可能不会感兴趣。任务局部值允许我们在一个特定任务内的任何地方共享特定值——它们就像一个类型的静态属性,不同之处在于每个任务都有自己的值,而不是所有共享该属性的东西。分离的任务不会继承其父任务的 task local 值,因为它们没有父任务。
The actor context part is more important and more complex. When you create a regular task from inside an actor it will be isolated to that actor, which means you can use other parts of the actor synchronously:
参与者上下文 部分更重要、更复杂。当你从 Actor 内部创建常规任务时,它将与该 Actor 隔离,这意味着你可以同步使用 Actor 的其他部分:
actor User {
func authenticate(user: String, password: String) -> Bool {
// Complicated logic here
return true
}
func login() {
Task {
if authenticate(user: "taytay89", password: "n3wy0rk") {
print("Successfully logged in.")
} else {
print("Sorry, something went wrong.")
}
}
}
}
let user = User()
await user.login()
try? await Task.sleep(for: .seconds(0.5))
In comparison, a detached task runs concurrently with all other code, including the actor that created it – it effectively has no parent, and therefore has greatly restricted access to the data inside the actor.
相比之下,分离任务与所有其他代码(包括创建它的 Actor)同时运行——它实际上没有父级,因此极大地限制了对 Actor 内部数据的访问。
So, if we were to rewrite the previous actor to use a detached task, it would need to call authenticate()
like this:
因此,如果我们要重写前面的 actor 以使用分离的任务,则需要像这样调用 authenticate()
:
actor User {
func login() {
Task.detached {
if await self.authenticate(user: "taytay89", password: "n3wy0rk") {
print("Successfully logged in.")
} else {
print("Sorry, something went wrong.")
}
}
}
func authenticate(user: String, password: String) -> Bool {
// Complicated logic here
return true
}
}
let user = User()
await user.login()
This distinction is particularly important when you are running on the main actor, which will be the case if you’re responding to a button click for example. The rules here might not be immediately obvious, so I want to show you some examples of what is allowed and what is not allowed, and more importantly explain why each is the case.
当您在 main actor 上运行时,这种区别尤其重要,例如,如果您正在响应按钮单击,就会出现这种情况。这里的规则可能不是很明显的,所以我想向您展示一些允许和不允许的示例,更重要的是解释为什么会这样。
First, if you’re changing the value of an @State
property, you can do so using a regular task like this:
首先,如果要更改 @State
属性的值,可以使用如下所示的常规任务来实现:
struct ContentView: View {
@State private var name = "Anonymous"
var body: some View {
VStack {
Text("Hello, \(name)!")
Button("Authenticate") {
Task {
name = "Taylor"
}
}
}
}
}
Note: The Task
here is of course not needed because we’re just setting a local value; I’m just trying to illustrate how regular tasks and detached tasks are different.
注意: 这里的 Task
当然不是必需的,因为我们只是设置了一个本地值;我只是想说明常规任务和分离任务的不同之处。
If we try and do the same thing from a detached task, Swift will complain that it’s not safe, even though one would hope that @State
should be able to synchronize with the main actor just fine.
如果我们尝试从一个分离的任务中做同样的事情,Swift 会抱怨它不安全,即使人们希望 @State
应该能够很好地与主要参与者同步。
The rules are similar when we switch to an object that uses the @Observable
macro to publish changes: because the entire SwiftUI view runs on the main actor, any changes made to the @Observable
object from there will also run on the main actor.
当我们切换到使用 @Observable
宏发布更改的对象时,规则类似:由于整个 SwiftUI 视图在主角色上运行,因此从那里对 @Observable
对象所做的任何更改也将在主角色上运行。
So, this kind of code is safe:
所以,这种代码是安全的:
@Observable
class ViewModel {
var name = "Anonymous"
}
struct ContentView: View {
@State private var model = ViewModel()
var body: some View {
VStack {
Text("Hello, \(model.name)!")
Button("Authenticate") {
Task {
model.name = "Taylor"
}
}
}
}
}
However, we cannot use Task.detached
here, because we’re passing a property made on the main actor into a task running on a different actor entirely.
但是,我们不能在这里使用 Task.detached
,因为我们将对 main actor 创建的属性传递到在完全不同的 actor 上运行的任务中。
At this point, you might wonder why detached tasks would have any use. Well, consider this code:
此时,您可能想知道为什么分离的任务会有任何用处。好吧,考虑一下这段代码:
struct ContentView: View {
var body: some View {
Button("Authenticate", action: doWork)
}
func doWork() {
Task {
for i in 1...10_000 {
print("In Task 1: \(i)")
}
}
Task {
for i in 1...10_000 {
print("In Task 2: \(i)")
}
}
}
}
That’s the simplest piece of code that demonstrates the usefulness of detached tasks: a SwiftUI view with a button that launches a couple of tasks to print out text.
这是演示分离任务的有用性的最简单的代码:一个带有按钮的 SwiftUI 视图,该按钮可启动几个任务以打印出文本。
When that runs, you’ll see “In Task 1” printed 10,000 times, then “In Task 2” printed 10,000 times – even though we have created two tasks, they are executing sequentially. This happens because our SwiftUI views always run on the main actor – it’s literally baked into the View
protocol – and so only one task can run at a time.
当它运行时,您将看到“在任务 1 中”打印了 10,000 次,然后“在任务 2 中”打印了 10,000 次 – 即使我们创建了两个任务,它们也是按顺序执行的。发生这种情况是因为我们的 SwiftUI 视图始终在主参与者上运行 – 它实际上已经融入到 View
协议中 – 因此一次只能运行一个任务。
In contrast, if you change both Task
initializers to Task.detached
, you’ll see “In Task 1” and “In Task 2” get intermingled as both execute at the same time. Without any need for actor isolation, Swift can run those tasks in parallel – using a detached task has allowed us to shed our attachment to the main actor.
相反,如果将两个 Task
初始值设定项都更改为 Task.detached
,则会看到“In Task 1”和“In Task 2”混合在一起,因为两者都同时执行。无需任何 actor 隔离,Swift 可以并行运行这些任务 —— 使用分离的任务使我们能够摆脱对 main actor 的依恋。
Although detached tasks do have very specific uses, generally I think they should be your last port of call – use them only if you’ve tried both a regular task and async let
, and neither solved your problem.
尽管分离任务确实有非常具体的用途,但通常我认为它们应该是你的最后一个停靠港——只有当你尝试过常规任务和async let
并且都没有解决你的问题时,才使用它们。