Are actors thread-safe?

The Actor Reentrancy Problem in Swift

no. Consider the situation of reentrance

Is the suspension point in the actor? If so, then something may happen between two concurrent tasks 重入(Reentrance).

for example:

actor BankAccount {
    
    private var balance = 1000
    
    func withdraw(_ amount: Int) async {
        
        print("🤓 Check balance for withdrawal: (amount)")
        
        guard canWithdraw(amount) else {
            print("🚫 Not enough balance to withdraw: (amount)")
            return
        }
        
        guard await authorizeTransaction() else {
            return
        }
        
        print("✅ Transaction authorized: (amount)")
        
        balance -= amount
        
        print("💰 Account balance: (balance)")
    }
    
    private func canWithdraw(_ amount: Int) -> Bool {
        return amount <= balance
    }
    
    private func authorizeTransaction() async -> Bool {
        
        // Wait for 1 second
        try? await Task.sleep(nanoseconds: 1 * 1000000000)
        
        return true
    }
}
​
let account = BankAccount()
​
Task {
    await account.withdraw(800)
}
​
Task {
    await account.withdraw(500)
}

We can imagine what the output of this will be? 3, 2, 1

At first glance, we would think that the execution is completed first await account.withdraw(800), and then there is 200 left, and then the execution is executed await account.withdraw(500). It is found that the balance is insufficient and the execution is not executed.

But the result is as follows:

🤓 Check balance for withdrawal: 800
🤓 Check balance for withdrawal: 500
✅ Transaction authorized: 800
💰 Account balance: 200
✅ Transaction authorized: 500
💰 Account balance: -300

You see, the balance has become -300, which is obviously inconsistent with our knowledge. So why does the result appear? In fact, it is because the actor can only guarantee that there will be no data competition, but our withdrawmethods will authorizeTransactionhang, which means that we do not finish executing one task before executing another. . In other words, it will not judge when it is suspended. The variable state will still maintain the state when you just entered the task. This may be a bit confusing. Let’s take this example as an example. The actor will not guarantee that the balance will be automatically changed to task1. The value after execution is 200. So balance -= amountwhen we are doing the operation, we need to judge whether the balance meets the conditions. This is what Apple recommends that we do, that is, always keep the actor’s status changes synchronously.

Right now:

func withdraw(_ amount: Int) async {
    
    // Perform authorization before check balance
    guard await authorizeTransaction() else {
        return
    }
    print("✅ Transaction authorized: (amount)")
    
    print("🤓 Check balance for withdrawal: (amount)")
    guard canWithdraw(amount) else {
        print("🚫 Not enough balance to withdraw: (amount)")
        return
    }
    
    balance -= amount
    
    print("💰 Account balance: (balance)")
    
}

However, the hypothetical authorizeTransactionmethod is very time-consuming, and the balance obviously does not meet the deduction conditions, which is a waste of resources. So we can:

func withdraw(_ amount: Int) async {
    
    print("🤓 Check balance for withdrawal: (amount)")
    guard canWithdraw(amount) else {
        print("🚫 Not enough balance to withdraw: (amount)")
        return
    }
    
    guard await authorizeTransaction() else {
        return
    }
    print("✅ Transaction authorized: (amount)")
    
    // Check balance again after the authorization process
    guard canWithdraw(amount) else {
        print("⛔️ Not enough balance to withdraw: (amount) (authorized)")
        return
    }
​
    balance -= amount
    
    print("💰 Account balance: (balance)")
    
}

Although it is done one more time canWithdraw, it can improve efficiency and avoid the problem of re-entry.

Leave a Reply

Your email address will not be published. Required fields are marked *