Task.detached doesn’t allow implicit self

I noticed that the Task.detached method does not have the @_implicitSelfCapture attribute that Task.init has. (Stdlib source: https://github.com/apple/swift/blob/230dfcc30c6ed24ed84e1e1ffb371415053c1617/stdlib/public/Concurrency/Task.swift#L684-L687)

And sure enough, the compiler does force you to use explicit self in a Task.detached closure.

Is this intentional or an oversight? SE-304: Structured Concurrency says implicit self should also apply to Task.detached:

Closures passed to the Task initializer are not required to explicitly acknowledge capture of self with self..

Note: The same applies to the closure passed to Task.detached and TaskGroup.addTask.

I found an earlier post that noted this: Why Task { } doesn’t need explicit use of self?:

I think detached task initially wasn't required to explicitly mention self but was later changed.

Questions

  1. Does detached task have a self requirement because of how it interacts with actors (it doesn't inherit the context and could run on a different executor)?
  2. Is there a way to reliably detect data races? TSAN sometimes catches it and sometimes seems to miss data races. (not sure others face this issue or I am missing something)

Does anybody have a source where this was decided (if it was formally decided)?

I could not find it through a quick search but I'm sure it was in one of the SE reviews of the initial concurrency Task primitives.

I believe the argument made was that "Task{} runs on the enclosing actor so causing weird retain cycles is less of an issue, you're on the actor and it must be alive already anyway", while Task.detached does not run on the enclosing actor so it's the same amount of problematic as random escaping closures. At least that's how i foggy remember it, I don't think I was very involved in this discussion. Maybe others remember or can find the discussion.

3 Likes

In practice I've run into retain cycles often just using Task {} because that's the first thing I reach for when setting up listening loops to AsyncStreams. Would it be better to use Task.detached{} instead? Still, I've been hesitant to do that because I wanted to capture the task locals and such. I find the implicit self to be frustrating and wish we didn't allow it.

Hm. I still can’t find any discussion about this in the forums (and there aren't too many results if you search for something like "detached implicit self", so I feel it should be findable).

Personally, I don't find this argument convincing. My understanding was always that implicit self capture exists because most tasks (whether detached or not) are not expected to be long-running/infinite, so any retain cycles would resolve automatically on task completion. I don't see why long-running/infinite tasks should be more or less lilely to be detached (if anything, maybe more likely?).


Anyway, as far as I can tell, this is the pull request that removed the @_implicitSelfCapture for detached tasks: [Concurrency] Alternative Task API. by DougGregor · Pull Request #37495 · apple/swift · GitHub

This PR also introduced the Task { … } and Task.detached { … } APIs for the first time, so it's a big change and difficult to parse. But:

This, plus the fact that SE-0304 says detached tasks should capture self implicitly, makes me wonder if it was indeed an oversight. cc @Douglas_Gregor

2 Likes