5

When creating a Task in a task-less context (e.g. from some random AppKit code), is there a difference between creating a detached or a regular task? For example, calling

  • Task.detached
  • or Task.init

Since the task has no actor to inherit from, I figured those 2 calls must be equivalent or are there still implementation differences to consider?

  • Umm... neither of those create a child task? – Sweeper Mar 22 '22 at 17:01
  • If you create a `Task` inside an existing task, it will be the child of that one. Otherwise there would be no point to `Task.detached`. –  Mar 22 '22 at 17:05
  • [That's not what the documentation says...](https://developer.apple.com/documentation/swift/task/3856791-init) "Run given operation as asynchronously in its own *top-level* task." `Task.init` copies over the current priority and actor context (`detached` does not). Child tasks do that too, but that doesn't mean `Task.init` create child tasks. `await someAsyncFunc()` and `async let` etc are what create child tasks. – Sweeper Mar 22 '22 at 17:10
  • `inherits the priority and actor context of the caller` -> ergo: "child" task. Also tasks created with `Task.init` are cancelled when the parent is cancelled unlike `detached` tasks. –  Mar 22 '22 at 17:15
  • Era, it sounds like you understand the difference, so I'm not sure if I understand your question. IMHO, detached tasks are designed to solve a very specific problem, to opt the task out of structured concurrency and/or get it off the current thread. I would only do this when I need these specific behaviors, and otherwise use `Task.init`. Using detached tasks unnecessarily seems imprudent. But where needed, then certainly use it. The threading behaviors of the two are different, so I would not consider them equivalent. – Rob Mar 25 '22 at 16:56
  • Thats why I am asking, because it is not documented if it makes a difference if used from a context without a specific actor. So the real question I guess is, if any regular thread has an implicit actor. –  Mar 26 '22 at 09:02
  • Understood. A data point from my empirical tests: If I initiate a `Task { … }` and `Task.detached { … }` from a non-swift-concurrency random background GCD worker thread, it behaves as if it were initiated from the main actor (namely that the former runs on the main thread and the latter on a background thread, regardless). So it’s not a function of the thread from which you initiate it, but some other, broader context. My suspicion is that an app just runs on the main actor by default (though, I confess, I have not yet found the relevant explicit `@MainActor` decoration in the headers)… – Rob Mar 26 '22 at 20:24
  • I summarized my findings in this post: https://stackoverflow.com/a/73015435/8220920 – Michael Bernat Jul 18 '22 at 09:57

1 Answers1

9

First of all, it is important to note that neither of these create child tasks. A child task is created by structured concurrency constructs like awaiting an async function, async let, or task groups, as opposed to unstructured concurrency like Task.init and Task.detached.

In addition to the structured approaches to concurrency described in the previous sections, Swift also supports unstructured concurrency. [...] To create an unstructured task that runs on the current actor, call the Task.init(priority:operation:) initializer. To create an unstructured task that’s not part of the current actor, known more specifically as a detached task, call the Task.detached(priority:operation:) class method.

This is also documented in Task.init (my bold)

Runs the given nonthrowing operation asynchronously as part of a new top-level task on behalf of the current actor.

You can also see this in action - a child task created by awaiting an async function is cancelled when its parent is cancelled, but a task created by Task.init is not.

let task = Task {
    let notChild = Task {
        await Task.sleep(1_000_000_000)
        if !Task.isCancelled {
            print("Not Child!")
        } else {
            print("Child!")
        }
    }
}
task.cancel()
// prints "Not Child!"
func childTask() async {
    await Task.sleep(1_000_000_000)
    if !Task.isCancelled {
        print("Not Child!")
    } else {
        print("Child!")
    }
}

let task = Task {
    await childTask()
}
task.cancel()
// prints "Child!"

Anyway, back to your question, so what actually is the difference between Task.init and Task.detached? The first quote in my answer addressed that a little bit, and this is also mentioned in the documentation of Task.init too:

Like Task.detached(priority:operation:), this function creates a separate, top-level task. Unlike Task.detached(priority:operation:), the task created by Task.init(priority:operation:) inherits the priority and actor context of the caller, so the operation is treated more like an asynchronous extension to the synchronous operation.

Essentially, it's about the actor on which the task is executed, so even if there is no current task, but there is a current actor, Task.init and Task.detached will still do different things. Task.init will run the task on the current actor, whereas Task.detached will run the task detached from any actor.

See also: https://www.hackingwithswift.com/quick-start/concurrency/whats-the-difference-between-a-task-and-a-detached-task

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • As I said, you did not understand what I was asking... this is not it. I was asking what difference `Task.init` and `Task.detached` makes coming from a **task-less** (toplevel) context. –  Mar 22 '22 at 19:31
  • 1
    @Era I did understand that part, yes. And I also addressed that part in the last paragraph. Whether you have a current task doesn’t matter as much as you think. It’s about the actor. – Sweeper Mar 22 '22 at 19:35
  • So there is basically no difference, since 99.9% of top level contexts are NOT actors. Or are you saying that creating a `Task` e.g. in `NSApplicationDelegate.applicationWillFinishLaunching` will refer to the main actor? What happens if a task is created on a random `DispatchQueue`? –  Mar 22 '22 at 19:36
  • @Era I would imagine that most of the time (this is from my experience) the main actor would be running the code. In other words, the code would be running on the main queue. Keep in mind that there are global actors! – Sweeper Mar 22 '22 at 19:38
  • This logic doesn't work for e.g. callbacks on a random global `DispatchQueue`. What actor would anyone expect there? This whole system is not thought through... –  Mar 22 '22 at 19:39
  • In any case, the answer is still irrelevant to my quesiton. Since we don't know what actor is used, it doesn't make sense. –  Mar 22 '22 at 20:50
  • Such a good answer @Sweeper – Fab1n Jun 22 '22 at 13:10