Architecture Note: Thread‑Safe Singleton in Objective‑C Using GCD dispatch_once
Learn how to build a thread‑safe singleton in Objective‑C with GCD’s dispatch_once, including verification steps and failure‑mode analysis.
03 Mar 2026, 18:56 UTC

Requirements
When a shared resource such as a network manager or database coordinator must be accessed from multiple threads, the initialization must happen exactly once to avoid race conditions, duplicate allocations, and inconsistent state.
Smallest Suitable Design
The minimal thread‑safe solution uses Grand Central Dispatch’s dispatch_once function, which guarantees that a block runs atomically once for the life of the process.
// Shared.h
@interface Shared : NSObject
+ (instancetype)sharedInstance;
@end
// Shared.m
@implementation Shared
+ (instancetype)sharedInstance {
static Shared *instance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
instance = [[self alloc] init];
});
return instance;
}
@end
Trust and Data Boundaries
The singleton’s only entry point is the class method sharedInstance. To keep the boundary, override init and new to return nil or raise an exception when called externally, preventing accidental allocation.
Operational Checks
Identity Equality Test
Dispatch several concurrent blocks and log the pointer returned by sharedInstance. All logs should show the same address.
dispatch_apply(8, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^(size_t idx) {
Shared *s = [Shared sharedInstance];
NSLog(@"%zu: %p", idx, s);
});
Thread Sanitizer
Enable Xcode’s Thread Sanitizer (Product → Scheme → Edit Scheme → Run → Diagnostics → Thread Sanitizer) and run the test above; TSan will report any data race on the static instance variable.
Initialization Log
Insert a NSLog inside the dispatch_once block to confirm it executes only once.
Failure Modes and Limitations
Recursive Deadlock
If the initialization block calls a method that eventually invokes sharedInstance again, the second call will wait for the first to finish, but the first cannot finish until the second returns, causing a deadlock.
Memory Leak Risk
Because the singleton lives for the process lifetime, holding strong references to short‑lived objects (e.g., view controllers) creates a permanent leak. Break those references or use weak references where appropriate.
When to Redesign
- If unit tests need to inject a mock, replace the singleton with a factory or dependency‑injection container.
- If the resource must be torn down and recreated (e.g., on user logout), use a regular object whose lifetime you manage instead of a static singleton.
- If initialization can fail and you need to retry,
dispatch_oncecannot be reset; use a serial queue or mutex with error handling.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.