Search code examples
iosswiftthread-safety

How can we make 'static' variables Thread-Safe in swift?


class MyClass {
     static var name: String = "Hello"
}

Static variables in swift are not thread-safe by default. If I want to make them thread-safe, how can I achieve that ?


Solution

  • Initialization of static variable is thread-safe. But if the object, itself, is not thread-safe, must synchronize your interaction with it from multiple threads (as you must with any non-thread-safe object, whether static or not).

    At the bare minimum, you can make your exposed property a computed property that manually synchronizes access to some private property. For example, with a lock (and NSLocking’s withLock):

    class MyClass {
        private static let lock = NSLock()
        private static var _name: String = "Hello"
    
        static var name: String {
            get { lock.withLock { _name } }
            set { lock.withLock { _name = newValue } }
        }
    }
    

    You can also use a GCD serial queue, reader-writer, or a variety of other mechanisms to synchronize. The basic idea would be the same, though: Manually synchronize your properties (unless you use an actor, as described below).


    That having been said, it’s worth noting that this sort of property accessor synchronization is insufficient for mutable types. A higher level of synchronization is needed.

    Consider:

    let group = DispatchGroup()
    
    DispatchQueue.global().async(group: group) {
        for _ in 0 ..< 100_000 {
            MyClass.name += "x"
        }
    }
    
    DispatchQueue.global().async(group: group) {
        for _ in 0 ..< 100_000 {
            MyClass.name += "y"
        }
    }
    
    group.notify(queue: .main) {
        print(MyClass.name.count)
    }
    

    You’d think that because we have thread-safe accessors that everything is OK. But it’s not. This will not add 200,000 characters to the name. You’d have to do something like:

    class MyClass: @unchecked Sendable {
        static let shared = MyClass()
    
        private let lock = NSLock()
        private var _name: String = ""
    
        var name: String {
            get { lock.withLock { _name } }
        }
    
        func appendString(_ string: String) {
            lock.withLock {
                _name += string
            }
        }
    }
    

    Note, I have switched from static properties to a shared instance, so that I could make it @unchecked Sendable (for better interoperability with Swift concurrency).

    And then the following works:

    let group = DispatchGroup()
    
    DispatchQueue.global().async(group: group) {
        for _ in 0 ..< 100_000 {
            MyClass.shared.appendString("x")
        }
    }
    
    DispatchQueue.global().async(group: group) {
        for _ in 0 ..< 100_000 {
            MyClass.shared.appendString("y")
        }
    }
    
    group.notify(queue: .main) {
        print(MyClass.shared.name.count)
    }
    

    Or, nowadays, we might use an actor, which is simpler, with no manual synchronization required:

    actor MyActor {
        static let shared = MyActor()
    
        var name: String = ""
    
        func appendString(_ string: String) {
            name += string
        }
    }
    

    This is simpler and is the contemporary Swift concurrency approach. But the idea is to provide a getter for the property, and then synchronize the mutation of this property.

    The other classic example where this sort of synchronized updates is necessary is where you have two properties that related to each other, for example, maybe firstName and lastName. You cannot just make each of the two properties thread-safe, but rather you need to make the single task of updating both properties thread-safe.

    These are silly examples, but illustrate that sometimes a higher level of abstraction is needed. But for simple applications, synchronizing the computed properties’ accessor methods may be sufficient.


    As a point of clarification, while statics, like globals, are instantiated lazily, standard stored properties bearing the lazy qualifier are not thread-safe. As The Swift Programming Language: Properties warns us:

    If a property marked with the lazy modifier is accessed by multiple threads simultaneously and the property hasn’t yet been initialized, there’s no guarantee that the property will be initialized only once.