The reentrancy problem

What is it and how to solve it?

by Krisztián
December 16, 2024

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.

a guard in front of a door

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