Explicit self not required for Task closures?

I think this is a bug, but I would like confirmation. The following code compiles cleanly, whereas I think it should error due to lack of explicit self

final class Foo {
    var thisVariableInvolvesSelf = 42
    
    func leak1() {
        Task {
            thisVariableInvolvesSelf += 1
        }
    }
}

In the Swift book, it says that escaping closures require an explicit self:

If you want to capture self , write self explicitly when you use it, or include self in the closure’s capture list.

I believe Task {} is actually the following constructor which takes an @escaping parameter

public init(priority: TaskPriority? = nil, operation: @escaping @Sendable () async -> Success)

As a consequence of this behavior, in the following example, the closure strongly retains self even though [weak self] is in the capture list. As a corollary, the function leaks.

final class Bar {
    var thisVariableInvolvesSelf = 43
    func leak2() {
        Task { [weak self] in
            //never yields
            await withCheckedContinuation({(_: CheckedContinuation<(),Never>) in })
            thisVariableInvolvesSelf += 1
            self?.innocent()
        }
    }
    func innocent() { }
}

This leak has caused me some headache. I do not really understand the mechanism of it, unless I there is some alternative constructor for Task which takes a nonescaping closure.

2 Likes

Is that a leak, or is the capture just extending the lifetime of self? Once the Task completes the memory is released, right?

In the motivating situation (and in example 2) the Task will never complete so there is no distinction between the two.

In the motivating case the task involves looping over an AsyncSequence of incoming push notifications, which is an unbounded sequence.

True enough, though I dislike conflating leaks with extended reference lifetimes, since the lifetime would end given the rest of the code is executed. In any case, does a newer toolchain affect the behavior at all? I wonder if this is a bug in 5.5 that was fixed later.

Task init has a underscored annotation so self is not required even tho os escaping. Xcode doesn’t show it but you can see it in the Swift source.

1 Like

The latest toolchain also produces a warning, though I don't think it's really a very helpful one:

Capture of 'self' with non-sendable type 'Foo' in a @Sendable closure

1 Like

The warning is unrelated, it just wants concurrency safety

actor Foo {
	var thisVariableInvolvesSelf = 42
	
	func leak1() {
		Task {
			thisVariableInvolvesSelf += 1
		}
	}
}


actor Bar {
	var thisVariableInvolvesSelf = 43
	func leak2() {
		Task { [weak self] in
			//let the task finish after all!
			//  await withCheckedContinuation({(_: CheckedContinuation<(),Never>) in })
			thisVariableInvolvesSelf += 1
			await self?.innocent()
		}
	}
	func innocent() { }
}

On the semantic question, I believe it is a real leak. In practice, it leaks even if you finish the task. My theory of the cycle is as follows

  1. instance-of-Bar retains
  2. Task, which retains
  3. anonymous closure, which retains
  4. 1, through the implicit self

I suspect it is an implementation detail whether finishing the task breaks 2->3, but in practice it doesn't.

Looks like this issue with @_implicitSelfCapture was anticipated by @Douglas_Gregor in the review of SE-0304:

Explicit self is there for a specific purpose: to help identify retain cycles by requiring one to explicitly call out self capture where it's likely to cause retain cycles. We intentionally did not make it required everywhere , and then later we removed explicit self in other unlikely scenarios . When capturing self in a task, you effectively need the task to have an infinite loop for that capture to be the cause of a reference cycle. That fits very well with the direction already set for explicit self .

Though perhaps the necessity of having infinite loops in Tasks was not as anticipated yet since AsyncSequence wasn't fully established. But since throwing off a task to handle an infinite sequence will be rather common, it certainly appears this point needs to be reexamined.

At the very least, allowing implicit capture even after the user has initiated manual capture seems like a bug, so that should be filed.

3 Likes

That is very helpful, thanks! It the infinite loop question but I believe that is something of a red herring actually.

Testing on my end suggests the examples I posted still leak whether the task completes or not, which seems to contradict

you effectively need the task to have an infinite loop for that capture to be the cause of a reference cycle.

That part definitely seems like a bug, regardless of the other behavior, and in addition to manual self capture not overriding the implicit capture.

6 Likes

When setting a variable value from within a Task that's inside a SwiftUI View, I am receiving the warning:

Capture of 'self' with non-sendable type 'ContentView' in a @Sendable closure

struct ContentView: View {
    @State private var isAuthenticating = false

    private func onCompletion() {
        Task {
            await MainActor.run {
                self.isAuthenticating = false
            }
        }
    }
}

Is this the same bug? This is Xcode Version 13.3 beta (13E5086k).

Apparently ‘ @ unchecked Sendable’ is our friend. :face_with_monocle:

I also request "requiring explicit self capturing" on Task closure.

Today I stumbled on a difficult bug and the root cause was unintentionally (implicitly) captured self in Task closure. Retain cycle or "unexpectedly prolonged lifetime" is one of the hardest, time-consuming bug to find out and fix in RC system.

On Swift, I can forget the risk of implicitly captured self because compiler checks it for me. If implicit self capturing is allowed, I cannot forget about it anymore. Getting more things to care and remember is really painful experience. Please consider this pain more seriously.

5 Likes

Never completing streams are absolutely going to be a thing with AsyncSequence, therefore this is a massive footgun

Setting Build Setting > Strict Concurrency Checking > Complete throws the following warning (on the original code):

Capture of 'self' with non-sendable type 'Foo' in a `@Sendable` closure

Build Setting: Strict Concurrency Checking > Complete
Xcode: Version 14.0 beta 3 (14A5270f)
Swift: Swift 5.7

1 Like

Might be worth a read: self is no longer required in many places – available from Swift 5.3