18

I have created an simple singleton in Swift 3:

class MySingleton {
    private var myName: String
    private init() {}
    static let shared = MySingleton()

    func setName(_ name: String) {
        myName = name
    }

    func getName() -> String {
        return myName
    }
}

Since I made the init() private , and also declared shared instance to be static let, I think the initializer is thread safe. But what about the getter and setter functions for myName, are they thread safe?

Abizern
  • 146,289
  • 39
  • 203
  • 257
Leem.fin
  • 40,781
  • 83
  • 202
  • 354
  • I think it is not thread safe even it is singleton or not – Hieu Dinh Aug 16 '17 at 09:48
  • How to make it thread safe? – Leem.fin Aug 16 '17 at 10:49
  • Add a serial dispatch queue as a member of the class, and enqueue get/set operations on it – Alexander Aug 16 '17 at 12:40
  • "Dispatch barriers" to handle the Readers-Writers Problem: https://www.raywenderlich.com/148513/grand-central-dispatch-tutorial-swift-3-part-1 – Ashok Dec 24 '17 at 11:47
  • It may be worth considering - does it matter? Making this thread safe with barriers etc. assumes two different threads are dependent on each others execution order. There are cases there his makes clear sense (like producer/consumer patterns), but the less threads depend on each other the better. – emp Nov 10 '18 at 00:54

2 Answers2

32

A slightly different way to do it (and this is from an Xcode 9 Playground) is to use a concurrent queue rather than a serial queue.

final class MySingleton {
    static let shared = MySingleton()

    private let nameQueue = DispatchQueue(label: "name.accessor", qos: .default, attributes: .concurrent)
    private var _name = "Initial name"

    private init() {}

    var name: String {
        get {
            var name = ""
            nameQueue.sync {
                name = _name
            }

            return name
        }
        set {
            nameQueue.async(flags: .barrier) {
                self._name = newValue
            }
        }
    }
}
  • Using a concurrent queue means that multiple reads from multiple threads aren't blocking each other. Since there is no mutation on getting, the value can be read concurrently, because...
  • We are setting the new values using a .barrier async dispatch. The block can be performed asynchronously because there is no need for the caller to wait for the value to be set. The block will not be run until all the other blocks in the concurrent queue ahead of it have completed. So, existing pending reads will not be affected while this setter is waiting to run. The barrier means that when it starts running, no other blocks will run. Effectively, turning the queue into a serial queue for the duration of the setter. No further reads can be made until this block completes. When the block has completed the new value has been set, any getters added after this setter can now run concurrently.
allenh
  • 6,582
  • 2
  • 24
  • 40
Abizern
  • 146,289
  • 39
  • 203
  • 257
  • 3
    I really like this variation a lot. As far as I understand it, it'll be just as "correct" in terms of data consistency. The only slight difference is the call semantics to the setter will be somewhat counterintuitive to a novice who might be expecting a delay while "acquiring the lock". Basically, "effectively set" and "in fact set" after the setter is executed, but that doesn't actually matter. – allenh Sep 12 '17 at 12:45
  • @AllenHumphreys, which approach to use, "yours" (serial) or "Abizern's" (barrier), depends on a lot of factors. Using barrier on a concurrent queue is not counterintuitive, it is different. It has some advantage and also some disadvantage. Sometimes both of them could fail. – user3441734 Sep 12 '17 at 13:53
  • Is this similar to running a `set` async and a `get` sync on a serial queue? – CyberMew Sep 18 '19 at 10:23
  • @CyberMew it isn’t the same thing. The point of using g a concurrent queue is so that if there are multiple reads, then the don’t block each other. Thief method I’ve outlined means the queue is concurrent while it is reading, but when it is writing it behave as a serial queue. – Abizern Sep 19 '19 at 12:22
  • I see, that’s good. If we needed the reads to happen after a write, can we still use this? – CyberMew Sep 19 '19 at 13:39
  • 1
    Yep. If you start to write, any readers added afterwards will be blocked until the write completes – Abizern Sep 20 '19 at 15:11
  • Yes! Thanks so much for this variation! Solved a lock-up that was driving me mad. – jbm Oct 27 '19 at 18:27
  • Using async will likely spawn threads in many cases, which is pretty expensive, and because the queue is concurrent you could end up with many threads if you have many concurrent set operations. More about the issues with this approach here: https://twitter.com/allenhumphreys/status/1516920652749590530 TLDR: better to use a serial queue like in the other answer. – Frederik Aug 25 '22 at 20:01
31

You are correct that those getters that you've written are not thread safe. In Swift, the simplest (read safest) way to achieve this at the moment is using Grand Central Dispatch queues as a locking mechanism. The simplest (and easiest to reason about) way to achieve this is with a basic serial queue.

class MySingleton {

    static let shared = MySingleton()

    // Serial dispatch queue
    private let lockQueue = DispatchQueue(label: "MySingleton.lockQueue")

    private var _name: String
    var name: String {
        get {
            return lockQueue.sync {
                return _name
            }
        }

        set {
            lockQueue.sync {
                _name = newValue
            }
        }
    }

    private init() {
        _name = "initial name"
    }
}

Using a serial dispatch queue will guarantee first in, first out execution as well as achieving a "lock" on the data. That is, the data cannot be read while it is being changed. In this approach, we use sync to execute the actual reads and writes of data, which means the caller will always be forced to wait its turn, similar to other locking primitives.

Note: This isn't the most performant approach, but it is simple to read and understand. It is a good general purpose solution to avoid race conditions but isn't meant to provide synchronization for parallel algorithm development.

Sources: https://mikeash.com/pyblog/friday-qa-2015-02-06-locks-thread-safety-and-swift.html What is the Swift equivalent to Objective-C's "@synchronized"?

allenh
  • 6,582
  • 2
  • 24
  • 40
  • It is worth to say that nothing like "the best way to achieve this in swift" exists. Thread safe or concurrent programming has no connection to used programming language and absolutely no connection with the singleton construction. The first question to answer must be not how but why you need to synchronize you data. If there is no some advantage it is better to avoid any kind of parallelism or concurrency. Your answer is almost correct, the question is very vague... – user3441734 Sep 11 '17 at 18:53
  • @user3441734 Since GCD (or other technologies) isn't available in all environments, I would argue that there is a connection. The question used the singleton as it's premise, but in the end the the question wasn't related to singletons. Saying it's "better to avoid any kind of parallelism or concurrency" is incredibly bad advice. In fact, to avoid concurrency in iOS is more of a challenge than simply embracing it and understanding it. – allenh Sep 12 '17 at 13:27
  • i wrote "If there is no some advantage it is better to avoid ..." I am a big fun of GCD and i use it in almost all my projects. – user3441734 Sep 12 '17 at 13:43
  • 2
    Just be careful that something like `name += "bar"` is not thread safe as it consists of a read and a write operation, where another thread could modify name in between those two operations, like described in this nice article: https://www.objc.io/blog/2018/12/18/atomic-variables/ – d4Rk May 07 '19 at 08:45
  • Very useful article, very recent as well, thanks a lot @d4Rk! – CyberMew Sep 18 '19 at 10:20
  • I hit what appears to be a deadlock using this approach recently. It only seems to happen in the simulator, but I found it while debugging a crash on the device. It seems related to the sync condition in the getter. Any ideas? – jbm Oct 21 '19 at 13:27