somu
(somu)
1
Hi,
I have some confusion around sendable closures
My understanding (could be wrong):
- Types need to be sendable to be able to safely accessed across concurrency domains.
- For a unstructured task / detached task, the closure needs to be sendable
- For a closure to be sendable all the types in the closure (parameter and return types need to be sendable)
-
@Sendable attribute wouldn't be required (or wouldn't make a difference) for a closure that only contains sendable types.
Questions
- Is my understanding correct?
- When non-sendable types are used in @Sendable closure, what happens? (It is even allowed to be modified)
- Is there more documentation around this? I found Apple Developer Documentation but uses them only with sendable types.
@Sendable attribute:
I tried using @Sendable attribute on a closure containing non-sendable type
Note:
- There were no compiler warnings or errors even when Build Setting > Strict Concurrency Checking > Complete was set.
Code
import Foundation
class Car {
var name: String
init(name: String) {
print("car being created")
self.name = name
}
}
func f1(closure: @Sendable @escaping (Car) -> ()) {
Task {
let car = Car(name: "aaa")
print("original car = \(ObjectIdentifier(car)), car.name = \(car.name)")
closure(car)
}
}
f1 { car in
print("f1 car = \(ObjectIdentifier(car)), car.name = \(car.name)")
car.name = "bbb"
print("f1 car = \(ObjectIdentifier(car)), car.name = \(car.name)") //allowed to modify
}
RunLoop.main.run()
Output
car being created
original car = ObjectIdentifier(0x000060000022f020), car.name = aaa
f1 car = ObjectIdentifier(0x000060000022f020), car.name = aaa
f1 car = ObjectIdentifier(0x000060000022f020), car.name = bbb
lukasa
(Cory Benfield)
2
No. Specifically this part is wrong:
A closure can be @Sendable even if its parameter and return types are not sendable. What @sendable implies is only that the closure must not close over non-Sendable types. It's totally fine for this closure to be passed a non-Sendable type as an argument, or to return one.
4 Likes
somu
(somu)
3
Thanks a lot @lukasa, I have some doubts.
What did you mean by "close" in the following sentence?
lukasa
(Cory Benfield)
4
A closure is conceptually a special kind of function. A regular function is a straightforward computation over its inputs, returning its outputs. A closure is augmented by having what we often call a "closure context", which is some collection of data that the closure can access. This data is derived from the scope in which the closure is declared.
We can use this as an example:
let x = 1
let y = 2
let myArray = [1, 2, 3, 4]
myArray.map { $0 + x }
In this example, the closure is syntactically the region of code that reads { $0 + x }. The type of this closure is (Int) -> Int: that is, it accepts one argument of type Int and returns an argument of type Int.
However, this is not sufficient to describe this closure fully, because this closure contains a reference to a variable x that was not defined within the closure! This variable forms part of what is called the "closure context", and we would say that "the closure passed to map closes over x. Importantly, closures do not close over everything that is in scope where they are defined: our closure above does not close over y.
In this instance, then, if map required a @Sendable closure then it would only be allowed to have a closure context that contained Sendable types. The arguments and return values are covered separately, and are not required to be Sendable.
8 Likes
somu
(somu)
5
Thanks a ton! @lukasa
That is a very clear explanation, I had completely gotten it wrong.
Now my understanding
- So sendable closure should not close over (capture by reference) any variable (whether the type of the variable is sendable or not (eg. Int))
- It is fine to access the constants
class A {
var price1 = 10
func f1() {
var price2 = 20
let price3 = 30
Task {
print(price1) //Capture of 'self' with non-sendable type 'A' in a `@Sendable` closure
print(price2) //Reference to captured var 'price2' in concurrently-executing code; this is an error in Swift 6
print(price3) //Valid since price3 is a constant
}
}
}
Note:
Used the Build Setting > Strict Concurrency Checking > Complete
somu
(somu)
6
It would be awesome if your explanation was included in the documentation for Sendable Apple Developer Documentation
I have filed a feedback FB10786446
lukasa
(Cory Benfield)
7
We can add one extra piece of understanding: the closure should not close over (capture by value) any non-Sendable type.
1 Like
somu
(somu)
8
But In our case price2 was a sendable type (Int) right?
xwu
(Xiaodi Wu)
9
In your case you are capturing a reference to price2, which is not the same as capturing price2 by value using a capture list: Task { [price2] in print(price2) }. To reiterate in your own words, both of these are true, non-overlapping statements:
1 Like
somu
(somu)
10
Thanks a lot @lukasa and @xwu for patiently answering my doubts.
I understand better now.
dnadoba
(David Nadoba)
11
As @lukasa has already explained, this is not true in the general case. This once confused me too and I therefore want to point out that there is actually one case where the sendability of your function arguments and return types matter. This is new in Swift 5.7 and part of the Sendability section in SE-338.
This proposal introduces a new rule:
the arguments and results of all async calls must be Sendable unless:
- the caller and callee are both known to be isolated to the same actor, or
- the caller and callee are both known to be non-actor-isolated.
On example is when you are in an actor and call out to a global function and pass in a non-sendable argument e.g.:
class Counter {
var counter: Int = 0
func increment() {
counter += 1
}
}
/// Counter is **not** Sendable
@available(*, unavailable)
extension Counter: Sendable {}
actor Foo {
let counter = Counter()
func increment() async throws {
// Non-sendable type 'Counter' exiting actor-isolated context in call to non-isolated global function 'incrementAfterOneSecond(counter:)' cannot cross actor boundary
try await incrementAfterOneSecond(counter: counter)
}
}
@Sendable func incrementAfterOneSecond(counter: Counter) async throws {
try await Task.sleep(nanoseconds: 1_000_000_000)
counter.increment()
}
2 Likes
somu
(somu)
12
Thanks @dnadoba
That's a good point, I think the following refers to async functions arguments / return types (not sendable closures).
As quoted above the reason could be because a non-sendable type is being accessed in non-isolated or different actor and that is not safe and is therefore not allowed (warning / error is thrown).
1 Like