Unable to send closure with an isolated parameter

I'm attempting to move from global isolation to a more-general isolated parameter. I thought for sure this was going to work but it does not. I have a guess as to what is going on here, but I'm looking for confirmation.

class NonSendable {
}

class TestThing {
	private let value = NonSendable()

	func notOk(isolation: isolated (any Actor)? = #isolation) {
		let op = {
			print(self.value)
		}

		Task {
			// ERROR: 'isolation'-isolated value of type '() async -> ()' passed as a strongly transferred parameter; later accesses could race
			op()
		}
	}

	@MainActor
	func ok() {
		let op = {
			print(self.value)
		}

		Task {
			op()
		}
	}
}

Capturing self in op is the key.

My guess is that in the ok example, self is implicitly Sendable because it is also implicitly MainActor-isolated. Or maybe op as a whole?

Either way, I don't fully understand why this is semantically different. How could later accesses race if the isolation both outside and inside of the Task in notOk is always the same?

I suspect that a possible explanation could be similar to what I've attempted to describe here: Ensuring _any_ isolation of a non-Sendable type - #2 by nkbelov

Calling notOk from actor A and then again from actor B captures op (and, in turn, the non-sendable self.value) into tasks isolated to two different actors.

2 Likes

Ahh, of course! Without the changes from Closure isolation control, I have to be really careful about how isolation is inherited within that Task.

Here's the solution:

	func notOk(isolation: isolated (any Actor)? = #isolation) {
		let op = {
			print(self.value)
		}

		Task {
			_ = isolation // this is critical to get the correct inheritance
			op()
		}
	}
3 Likes

TestThing is not Sendable though. It cannot be used from two different actors!

1 Like

Oh, my bad — I have been assuming for some reason that if an instance is protected by e.g. a global actor, like

@MainActor
static let testThing = TestThing()

this would make it so that other actors would still be able to reach out to it and call notOk each with their own isolation. Obviously, and luckily, this doesn't happen :upside_down_face:.

1 Like

Well, I'm cycled back to this same thing in just a slightly different form. This version, again, looks to me like it should work. But, the isolation capture in this case doesn't fix the issue and I don't fully understand why.

class NonSendable {
	public func noArgument(closure: () -> Void) {
	}

	public func isolatedNoArgument(isolation: isolated (any Actor)? = #isolation, value: Argument) {
		noArgument() {
			Task {
				_ = isolation

				// ERROR: Sending 'value' risks causing data races
				print(value)
			}
		}
	}

	@MainActor
	public func globalActorArgument(value: Argument) {
		noArgument() {
			Task {
				print(value)
			}
		}
	}
}

The use of the function + closure seems to be the issue. But, since the closure is non-Sendable and not even escaping, I'm not sure why the global isolation would make a difference.