Actor: Ensuring thread safety in concurrent code

Abhijith Krishnan
Klarna Engineering
Published in
6 min readFeb 17, 2023

--

Swift is packed with different mechanisms to achieve asynchronous operations in a structured way. We all need concurrent/parallel operations at least once during the course of development of an application. To ease the developer’s overhead for achieving concurrency/parallelism, Swift has two powerful mechanisms — GCD and OperationQueue. But even with these two, it can be cumbersome to handle complex scenarios. In this article we will discuss how thread safety can be ensured with traditional mechanisms vs Actor, and also look at the benefits of using Actor over GCD and OperationQueue.

My team joined Klarna through the acquisition of Stocard in 2021. With over 65 million users Stocard is one of the leading mobile wallets around the globe. Users can store loyalty cards from their favorite stores, access personalized offers, collect rewards and pay directly with the app.

Together, we are an ecosystem of 400+ developers working on our apps with over 125 million users.

Our main objective is to provide the best user experience along with great performance. During the course of developing the app to the finest, we had to deal with scenarios which could potentially create a race condition.

Let’s quickly look into such a scenario where we need to prevent a race condition for a Swift dictionary (Assuming backward compatibility from iOS 11).

We have a custom class which encapsulates regular operations of the dictionary powered by supporting helper methods.

1
2

Looks straightforward isn’t it ?

Let’s perform normal read/write operations on it.

3

Above block of code executes without any hassle in normal case and prints “1” as result. So far so good!!

However, what if this is going to happen for a complex concurrent operation.

4

Booom!!!!! We have a runtime crash due to bad access.

5

Also, if we have a look at the debug navigator, we can see how a single resource is being locked from different threads.

6

However, this can be easily resolved by leveraging GCD.

Let’s now introduce a custom concurrent queue on the custom dictionary.

7

In addition, slightly change the implementation of convenience helper methods by applying the golden rule of concurrency.

“Read mutable resource synchronously and write mutable resource asynchronously with a barrier block on a concurrent queue”

8

Now let’s re-run the block of code shown in Image 4. It will run as expected since the read/write operations are executed serially under the hood, and at a time only one consumer can access the resource.

I am pretty sure that we might have already faced this scenario and tackled it with the above solution. However, you might have noticed that to avoid race condition we have done quite a few steps as described below:

  • Created a custom concurrent queue
  • Brought all the read write operations of the dictionary inside the sync/async block of the queue
  • And it is a must to bring all additional read/write access of any operations of the dictionary to sync/async block (e.g. retrieving all elements, index of element, etc.)

Sometimes, it is a bit painful that we need to handle different queues and its sync/async blocks for tackling such situations, which would be even more difficult in complex scenarios.

Well, in WWDC2021 Apple has come up with a game changer modification to the concurrent/parallel programming by introducing the new model Actor. This is not just a replacement for the traditional mechanisms to manage concurrency but does much more.

So what is Actor?

As per Apple, Swift Actors are to protect mutable state in the code.

Basically, Actors are also like other Swift types (Struct, Class) as we can have initializers, methods, properties and subscripts. In addition, Actors can confirm to protocols and work with Generics. We can compare actors to classes, since Actor is also a generic type, but Actor cannot be a synchronous replacement for a Class. Unlike classes, Actors don’t support inheritance. Therefore, it is not possible to use statements like open, final and also it is not necessary to use keywords like override, required init, convenience init, etc.

Now let’s see how Actor works with the scenario discussed above, and whether it is more convenient and structured.

9

You will realise in Image 9 that the type has been changed from class to Actor, and our custom implementation of a concurrent safeQueue has been removed completely. Is this enough to support our scenario ? Yes!

When converting the whole SafeDictionary into Actor, Swift basically implements a lock mechanism under the hood. This optimized solution allows it’s consumers synchronised access to it’s isolated properties. As a result, the compiler prevents consumers from ending up in a data race scenarios.

10

Now let’s see, how it looks on a high level. More or less similar to the initial implementation except the Actor type. It’s time for trying out some read/write operations out of the new implementation.

Here we go..

11

Oops! The compiler throws some errors which we haven’t seen before. But, yes this is exactly why we have implemented Actor since we are forced to avoid referencing Actor’s isolated instance methods and properties outside its context in a non synchronised manner. So, to get rid of this we need to make the context as async/await.

The main benefit of it is, if any of the consumers are using the resource already, others have to wait. As soon as the resource has been released, the pending consumers will get access sequentially. Therefore, at a time only one thread can access mutable data from the Actor .

12

By making the whole context as an async/await we are following the rule and can ensure there is no datarace condition.

Besides that, we have removed Dispatch.concurrentPerform since it is part of a parallelism API and Swift 5.5 introduced concurrency instead of parallelism.Therefore, we have to use structured concurrency to perform tasks in parallel rather than using regular queues. In addition, the concurrentPerform is not an asynchronous operation, it will just block the caller thread until all the work is done within the block.

Yes! Now we do have something new to integrate into our Stocard environment and we are always eager to do so. Besides that, another strong reason for using Actors is that we have now deprecated iOS 13, and we don’t want to rely on traditional mechanisms unless absolutely necessary. This is just one of the many use cases which we have come across, but Actors and structured concurrency are really powerful and there is a lot more to explore.

Happy hacking!

Did you enjoy this post? Follow Klarna Engineering on Medium and LinkedIn to stay updated on more articles like this.

--

--