How Thread-Unsafe is lazy var in Swift?

Mohit Bhalla
2 min readFeb 25, 2025

When developing iOS applications, you might have come across lazy var in Swift. It's a convenient way to initialize properties only when they're first accessed, saving memory and speeding up app launch times. But what happens when multiple threads access a lazy var at the same time? Is it thread-safe? Let’s break it down.

What Makes lazy var Thread-Unsafe?

When you declare a property as lazy, Swift defers its initialization until it is accessed for the first time. The problem arises when multiple threads try to access the property at the same time, leading to race conditions. Since Swift does not provide built-in synchronization for lazy var, if multiple threads attempt to initialize the property simultaneously, it can result in unexpected behaviors such as:

  1. Multiple Initializations: The property might be initialized multiple times instead of just once.
  2. Partial Initialization: The property could end up in an inconsistent state if one thread partially initializes it while another thread tries to access it.
  3. Crash Due to Memory Corruption: If multiple threads modify the underlying memory storage simultaneously, it could cause crashes or undefined behavior.

Demonstrating the Issue

Consider the following code snippet:

class Example {
lazy var value: Int = {
print("Initializing value")
return 42
}()
}

let instance = Example()
DispatchQueue.global().async {
print(instance.value)
}
DispatchQueue.global().async {
print(instance.value)
}

In a multi-threaded environment, if multiple threads access instance.value at the same time, there's no guarantee that value will be initialized only once. This can lead to undefined behavior.

How to Make lazy var Thread-Safe

Since lazy var does not provide automatic thread safety, we need to implement synchronization manually. Here are a few approaches:

1. Use a Serial DispatchQueue

A common approach is to use a DispatchQueue to synchronize access to the lazy property:

class SafeExample {
private lazy var _value: Int = {
print("Initializing value")
return 42
}()
private let lockQueue = DispatchQueue(label: "com.example.lazyvar")

var value: Int {
return lockQueue.sync { _value }
}
}

This ensures that only one thread at a time can access _value, preventing race conditions.

2. Use NSLock

Another way to make lazy var thread-safe is by using NSLock:

class SafeExample {
private lazy var _value: Int = {
print("Initializing value")
return 42
}()
private let lock = NSLock()

var value: Int {
lock.lock()
defer { lock.unlock() }
return _value
}
}

3. Use atomic Property Wrapper (Swift Concurrency)

With Swift Concurrency, a better approach is to use @MainActor or @Sendable to ensure safe access:

@MainActor
class SafeExample {
lazy var value: Int = {
print("Initializing value")
return 42
}()
}

This ensures that value is always accessed on the main actor, making it safe from data races.

Conclusion

While lazy var is a powerful feature in Swift, it is not thread-safe by default. When working in a multi-threaded environment, you must manually synchronize access to ensure safe initialization. Using a DispatchQueue, NSLock, or Swift Concurrency features like @MainActor can help prevent race conditions and ensure reliable behavior in concurrent applications.

--

--

Mohit Bhalla
Mohit Bhalla

Written by Mohit Bhalla

Principle Engineer iOS, Wynk, Airtel | ex Hindustan times

No responses yet