Considering below dummy codes:
@MainActor var globalNumber = 0
@MainActor
func increase(_ number: inout Int) async {
// some async code excluded
number += 1
}
class Dummy: @unchecked Sendable {
@MainActor var number: Int {
get { globalNumber }
set { globalNumber = newValue }
}
@MainActor
func change() async {
await increase(&number) //Actor-isolated property 'number' cannot be passed 'inout' to 'async' function call
}
}
I'm not really trying to make an increasing function like that, this is just an example to make everything happen. As for why number
is a computed property, this is to trigger the actor-isolated
condition (otherwise, if the property is stored and is a value type, this condition will not be triggered).
Under these conditions, in function change()
, I got the error: Actor-isolated property 'number' cannot be passed 'inout' to 'async' function call
.
My question is: Why Actor-isolated property cannot be passed 'inout' to 'async' function call? What is the purpose of this design? If this were allowed, what problems might it cause?
Could it be because of the reentrancy? Because the increase
function is an explicit async function, there may be a suspension point inside it, which will cause reentrancy. This is just my guess, but I still can't come to my conclusion well.
The issue is a combination of two factors:
An inout
parameter of an async
method employs “copy-in copy-out” semantics. As The Swift Programming Language says:
In-out parameters are passed as follows:
- When the function is called, the value of the argument is copied.
- In the body of the function, the copy is modified.
- When the function returns, the copy’s value is assigned to the original argument.
This behavior is known as copy-in copy-out or call by value result. For example, when a computed property or a property with observers is passed as an in-out parameter, its getter is called as part of the function call and its setter is called as part of the function return.
An async
method can have an await
whereby, as you noted, actor reentrancy comes into play.
The combination of these two factors means that if the compiler permitted async
method with the inout
parameter, it would be possible for the copied value of the inout
parameter to not reflect concurrent changes (during reentrancy) to the original actor-isolated property.