Thread safe singleton in swift
Asked Answered
S

2

22

I have and Application which has a singleton that stores information across the whole app. However, this is creating some data race issues when using the singleton from different threads.

Here there is a very dummy and simplistic version of the problem:

Singleton

class Singleton {
    static var shared = Singleton()

    var foo: String = "foo"
}

Use of the singleton (from the AppDelegate for simplicity)

class AppDelegate: UIResponder, UIApplicationDelegate {

    var window: UIWindow?


    func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
        // Override point for customization after application launch.

        DispatchQueue.global().async {
            var foo = Singleton.shared.foo // Causes data race
        }

        DispatchQueue.global().async {
            Singleton.shared.foo = "bar"   // Causes data race
        }

        return true
    }
}

Is there any way to ensure that a singleton is thread safe, so it can be used from anywhere in the app without having to worry about which thread you are in?

This question is not a duplicate of Using a dispatch_once singleton model in Swift since (if I understood it correctly) in there they are addressing the problem of accessing to the singleton object itself, but not ensuring that the reading and writing of its properties is done thread safely.

Street answered 7/3, 2018 at 19:50 Comment(5)
See stackoverflow.com/questions/38373338/… for an example solution.Haldes
That seems to be working perfectly. Thanks for pointing me in the right direction!Street
Here's an even better solution (see the one using barrier sync for writes): https://mcmap.net/q/15354/-how-do-i-synchronize-access-to-a-property-that-has-didsetHaldes
Possible duplicate of Using a dispatch_once singleton model in SwiftLepanto
@Lepanto - He's not worried about any race with the singleton object itself. He's worried about synchronizing access to the singleton's properties, which is a different question.Tay
S
49

Thanks to @rmaddy comments which pointed me in the right direction I was able to solve the problem.

In order to make the property foo of the Singleton thread safe, it need to be modified as follows:

    class Singleton {

    static let shared = Singleton()

    private init(){}

    private let internalQueue = DispatchQueue(label: "com.singletioninternal.queue",
                                              qos: .default,
                                              attributes: .concurrent)

    private var _foo: String = "aaa"

    var foo: String {
        get {
            return internalQueue.sync {
                _foo
            }
        }
        set (newState) {
            internalQueue.async(flags: .barrier) {
                self._foo = newState
            }
        }
    }

    func setup(string: String) {
        foo = string
    }
}

Thread safety is accomplished by having a computed property foo which uses an internalQueue to access the "real" _foo property.

Also, in order to have better performance internalQueue is created as concurrent. And it means that it is needed to add the barrier flag when writing to the property.

What the barrier flag does is to ensure that the work item will be executed when all previously scheduled work items on the queue have finished.

Street answered 7/3, 2018 at 20:50 Comment(6)
nice QA, nikano !Scorpaenoid
If the same singleton has member foo2, do you protect it with the same internal queue, or with a different one?Ommatidium
I haven't test it, but my first answer would be to use the same internal queue, since it is "concurrent" if should work just fineStreet
@Street The class Singleton uses static shared: Singleton instance which is one and the same in all instances of class Singleton. But internalQueue object is different in every instance of the Singleton. How does iOS know that blocks submitted to different internalQueue objects should be serialized for writes? Apple docs imply that label is for debugging only. Should not internalQueue be static as well?Creditor
Why not use serial queue but use a concurrent queue instead?Griffis
Because a serial queue would not support multiple reads at the same time. Since we don't need to block access to just one read at a time, we use a concurrent queue to allow multiple reads. We do however need to support just one write a time, so we use the barrier flag.Philis
F
3

Swift Thread safe Singleton

[iOS Thread safe]

[GCD]

[Swift barrier flag for thread safe]

You are able to implement Swift's Singleton pattern for concurrent envirompment using GCD and 3 main things:

  1. Custom concurrent queue - local queue for better performance where multiple reads can be happened at the same time
  2. sync - customQueue.sync for reading a shared resource - to have clear API without callbacks
  3. barrier flag - customQueue.async(flags: .barrier) for writing operation: wait when running operations are done -> execute write task -> proceed executing task
public class MySingleton {
    public static let shared = Singleton()
    
    //1. custom queue
    private let customQueue = DispatchQueue(label: "com.mysingleton.queue", qos: .default, attributes: .concurrent)
    //shared resource
    private var sharedResource: String = "Hello World"

    //computed property can be replaced getters/setters
    var computedProperty: String {
        get {
            //2. sync read
            return customQueue.sync {
                sharedResource
            }
        }
        set {
            //3. async write
            customQueue.async(flags: .barrier) {
                sharedResource = newValue
            }
        }
    }
    
    private init() {
    }
}
Frech answered 16/2, 2021 at 23:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.