Thread-safe singletons and their usage in Swift

Sachithra Siriwardhane
7 min readJan 11, 2021

--

As developers, we all consume singletons, most of us are aware of the fundamentals of the singleton design pattern itself, and capable of implementing it in iOS applications. This article would help both beginners and for those who are willing to improve their knowledge in order to properly make use of the singleton design pattern.

This article covers:

  • Usage of singletons, the dos and don’ts
  • Singletons in Swift
  • Concurrency issues
  • Making singletons thread-safe
  • Readers-writer lock

Usage of singletons, the dos and the don’ts

The primary purpose of singletons is to make sure that we can create only one instance of a given type. Singletons work well when there is a single resource that needs to be accessed and managed throughout the application.

Singletons are commonly used in Apple’s platforms such as UIApplication, FileManager, UserDefaults, URLSession and UIAccelerometer. For example, an instance of UIApplication class should exist for an app but it should not have multiple instances. Following are some of the singletons in Apple platform:

UIApplication.shared, UserDefaults.standard, FileManager.default, URLSession.shared, OperationQueue.main

Misusing singletons

As singletons are relatively easy to implement, there are more chances of misusing them in practice. Among many, using singletons for passing data between views controllers, treating singletons as global multipurpose container are common examples of abusing this design pattern as it goes against the single responsibility principle (SOLID principles) which states that every class in a program should have responsibility over a single part of it’s functionality.

Moreover, once singletons are implemented, they are visible throughout the codebase, eventually loosing track of the objects which are depending on the singletons, consequently, increasing coupling. Furthermore, when singletons are heavily used throughout the application, changes in the singletons can come with a cost.

Additionally, singletons should be protected against concurrent usage in order to avoid synchronisation issues, and thread safe singletons could lead to a performance bottleneck if they’re being used by multiple threads in parallel, which can be avoided.

Singletons in Swift

The following code shows a basic singleton of EventLogger class which is prepared to collect data and to return them to relevant callers.

Basic implementation of a singleton

Here, the class EventLogger has been declared as final in order to prevent it from further subclassing.

public static let shared = EventLogger()

The constant shared property is the singleton object of EventLogger class, the static keyword makes the property belong to the class itself rather than instances of the class, making the property available globally.

private init() {}

The initialiser of the class has made private so as to prevent creating instances directly from outside of the class so it ensures that the initialiser can only be used within the class declaration.

The following code show how the singleton can be consumed. As the readLog returns an optional string, optional binding is used to unwrap the value and make the use of it.

Concurrency issues

In Swift, any variable declared with the let keyword is a constant and, therefore, read-only and thread-safe. When a variable is declared as var it becomes mutable and not thread-safe unless the data type is specifically designed to be thread-safe when mutable. Swift collection types such as Array and Dictionary are not thread-safe when they’re mutable.

Although the Dictionary in the above example is mutable but can be read by many threads simultaneously without any issue, it’s unsafe to allow one thread modify the Dictionary while another thread is reading it. The singleton in the above example doesn’t prevent this issue from happening and the solution is mentioned below.

What is dispatch queue? According to apple;

An object that manages the execution of tasks serially or concurrently on your app’s main thread or on a background thread.

Refer the apple documentation for further information on dispatch queues: https://developer.apple.com/documentation/dispatch/dispatchqueue

Concurrent Dispatch Queues: A concurrent queue executes multiple tasks at once, where the execution of them starts according to the order they’re added to the queue, but the execution of each task can be finished in a different order since those tasks can be executed in parallel. Dispatch queue manages the threads which tasks are executed on.

How to simulate concurrent usage of the above example?

For the demonstration, the concurrent execution can be written as a Unit Test.

The above code shows a unit test case written in order to test the concurrent usage of the singleton created earlier in this article.

At first, concurrentQueue is created which is capable of executing multiple writeToLog function calls at the same time in a parallel manner. Then, the for loop adds 50 writeToLog tasks to the queue, consequently, saving entries to the Dictionary. At the same time, readLog method is called repeatedly in order to see if both read and write commands execute properly when both are performed simultaneously.

Oops 🤔 ! The app crashed while running. Regardless of whether the current execution is a read or a write, it continued to crash in both methods, both while inserting to the Dictionary and while reading from it. The core issue is, since the Dictionary type is not thread-safe, the current implementation is missing protection against concurrent access.

Making singletons thread-safe

Serial Dispatch Queues: A serial dispatch queue executes only one task at a time. In most cases, serial queues are often used to synchronise access to a specific value or resource to prevent occurring data races.

As parallel and concurrent reads and writes produce data corruption, consequently causing the app to crash, making sure that both reads and writes do not execute in parallel, could be a solution. In other words, preventing both reads and writes occurring at the same time seems to solve the issue.

For that, serial queues are used as they guarantee the execution of tasks one at a time.

Using serial queue to prevent parallel execution

Both read and write code has been added to a serial queue so that it guarantees serial execution.

The test has succeeded after making the singleton thread safe 🤟. However, serialising the methods leads to performance issues and further optimisation is needed for this solution.

Readers-writer lock

A readers-writer lock allows concurrent access for read-only operations, while write operations require exclusive access. This means that multiple threads can read the data in parallel but an exclusive lock is needed for writing or modifying data. When a writer is writing the data, all other writers or readers will be blocked until the writer is finished writing. ~ Wiki

For further reading: https://en.wikipedia.org/wiki/Readers%E2%80%93writer_lock

A data race: Data race usually happens when the same memory is accessed by multiple threads at the same time without using any synchronisation mechanism and among those, at least one access is a write. A program could be reading values from a dictionary from the main thread while new values are being added to the same dictionary in the background thread.

A barrier: A barrier is used on a concurrent queue in order to synchronise writes. By indicating the barrier flag, it allows to make access to a certain resources or values thread-safe, hence, synchronising write access while keeping the benefit of concurrent reading.

There are cases where it is possible to benefit from concurrent queues to perform multiple tasks simultaneously while still preventing data races. Barriers do the needful here.

With concurrent queues, writing and reading from the dictionary shouldn’t happen in parallel. Concurrent read operations could be introduced as it leads to performance improvements.

Here, the serialisation of execution of read methods can be removed but simultaneous reading and writing to the dictionary needs to be prevented when the dictionary is already updating its contents. This update needs to complete before allowing other executions to perform on the dictionary. The Grand Central Dispatch solves the issue using dispatch barriers.

The barrier switches between concurrent and serial queues and performs as a serial queue until the code in barrier block finishes its execution and switches back to a concurrent queue after executing the barrier block.

In the above singleton class, serial queue is replaced with a concurrent queue and barrier block is introduced to writeToLog method as it updates the values in the dictionary. Here, the barrier block in writeToLog waits until all the previous blocks get completed. This prevents race conditions and data corruptions from occurring. readLog method only needs the concurrent queue as parallel execution for reads is allowed.

Added barrier for write operation

Now, the singleton is thread-safe and optimised for better performance 😃

Sources for further reading:

--

--