The reentrancy problem
What is it and how to solve it?
When a new feature is introduced in a programming language, we as developers are often quick to assume it will solve many of their challenges. The excitement to experiment with such features is natural, but it’s often accompanied by the realisation that every solution brings its own set of unique challenges. This was no different when actors were introduced.
A few words about actors
Actors are a (relatively) new reference type introduced in swift 5.5. Their main purpose is to solve, or better put, abstract out, thread safety in swift’s new advanced concurrency system. They achieve this by isolating members of an actor thus creating a controlled (synchronised) way of accessing them by code outside the isolation. This in practice means that different threads are only able to access the same member one by one. To better visualise this, imagine a room and a guard. The guard will only allow one person to access the room at any given moment and will make you wait outside until the previous person finishes their tasks and leaves.
While actors are really powerful and are a huge step up from all the previous tools we had (DispatchGroups, Operations, Locks, etc.) they are not bulletproof and while they do ensure thread safety, they can’t magically solve all the race conditions you may have…
What is the re-entrency problem
Let’s consider this piece of code
actor InventoryManager {
private var currentInventory: Int = 100
func retrieve(amount: Int) async -> Bool {
guard currentInventory >= amount
else { return false }
// In a real life scenario, you would
// also verify the success of this call
await authorisePayment()
currentInventoy -= amount
return true
}
}
As you can see, our method is intended to simulate an inventory management system. First, we check wether we have enough inventory for the incoming order, then we perform some async authorisation, then finally we decrease the inventory and return a success flag.
At first glance this code may seem rather simple and based on what we discuss earlier, you may have the impression that it is free of any bugs, after all, we said that actors ensure synchronised access right? Well, yes but no..
You see, the tricky part is our async call. Whenever we have an await keyword, the system creates a suspension point. At a suspension point, the execution of our code is.. suspended while we are waiting for our async call, in the meantime, other threads can call our method (enter the room) and modify stuff.
When our async call returns, it is not guaranteed at all that the condition (inventory >= order) is still true.
⚠️ Remember: You can never assume anything about the order of async operations. Even if you think it can't happen, it will!
How to get around this?
Solving this kind of data races is easier than you may think. You just have to make sure that there are no a suspension points between your condition and the code relaying on that condition
actor InventoryManager {
private var currentInventory: Int = 100
func retrieve(amount: Int) async -> Bool {
// In a real life scenario, you would
// also verify the success of this call
await authorisePayment()
guard currentInventory >= amount
else { return false }
currentInventoy -= amount
return true
}
}
💡 Note: that depending on you use case, you may be able to verify some conditions before the suspension point as well, to avoid expensive calls when you already know that the operation can not be performed anyways
I hope this article helped you clarify how you can avoid potential problems while working with actors. For any questions you may have, feel free to reach out to me on X or via email at contact@yenovi.com
Related articles
Here are some more articles that may interest you. Check them out!
Debugging memory leaks in Xcode
Ever got into a situation where you observed some strange behaviour with your app? Crashes, very bad performance or strange warnings in the console? Or simply you looked at the memory usage of your app and realised that it’s way too high for what your app should be doing? Yeah! Most likely you are dealing with a memory leak.
Read moreSimplify your code with @Entry
If you’ve ever tried to tap into the systems provided by SwiftUI, you are very likely to know, how quickly the boilerplate in your code can grow in size. The @Entry macro provides a solution to that
Read moreCustomise property coding with Property wrappers
Struggle with custom encoding/decoding for specific properties in your Codable models? This post explores how Property Wrappers can help eliminating the need for custom encoding / decoding in your types
Read more