0

I was playing around with a Singleton to generate unique ID's to conform to Hashable. Then I realised it wasn't really thread safe. So I switched to OSAtomicIncrement32 because it does not use a closure, so it can be as simple as this:

class HashID {

    static let sharedID = HashID()

    private var storedGlobalID : Int32 = 0
    var newID : Int {
        get {
            return Int(OSAtomicIncrement32(&storedGlobalID))
        }
    }

    private init() {}
}

Then I wanted to try a HashID for each Type. So I use _stdlib_getDemangledTypeName reflecting to feed a Dictionary. But then again it wasn't thread safe. Different threads can now manipulate the Dictionary at the same time.

class HashID {

    static let sharedID = HashTypeID()

    func idForType(type: Any.Type) -> Int {

        let typeName = _stdlib_getDemangledTypeName(type)

        guard let currentID = storedGlobalIDForType[typeName] else {

            let currentID : Int32 = 0
            storedGlobalIDForType[typeName] = currentID
            return Int(currentID)

        }

        let newID = atomicIncrement(currentID)
        storedGlobalIDForType[typeName] = newID
        return Int(newID)

    }

    private var storedGlobalIDForType : [String:Int32] = [String:Int32]()

    private func atomicIncrement(var value: Int32) -> Int32 {
        return OSAtomicIncrement32(&value)
    }

    private init() {}

}

I can obviously use GCD, but that means using closures and then I can't use a simple function with a return value. Using a completionHandler is just less "clean" and it also makes it more complicated to set this in the init of a class / struct.

This means default ID's and this would make them unhashable until the ID is fetched, or I would have to have an init with a completionHandler of it's own. -> forgot how dispatch_sync works.

As I said, I was just playing around with this code. The first function is perfectly fine. The second however also gives a count of all instances of a type that were created. Which is not the most useful thing ever...

Is there some way I am forgetting to make the access to the Dictionary thread safe?


Updated Code:

credits to Martin R and Nikolai Ruhe

class HashTypeID {
    // shared instance
    static let sharedID = HashTypeID()

    // dict for each type
    private var storedGlobalIDForType : [String:Int] = [String:Int]()

    // serial queue
    private let _serialQueue = dispatch_queue_create("HashQueue", DISPATCH_QUEUE_SERIAL)


    func idForType(type: Any.Type) -> Int {

        var id : Int = 0

        // make it thread safe
        dispatch_sync(_serialQueue) {

            let typeName = String(reflecting: type)

            // check if there is already an ID
            guard let currentID = self.storedGlobalIDForType[typeName] else {

                // if there isn't an ID, store one
                let currentID : Int = 0
                self.storedGlobalIDForType[typeName] = currentID
                id = Int(currentID)

                return
            }

            // if there is an ID, increment
            id = currentID
            id++

            // store the incremented ID
            self.storedGlobalIDForType[typeName] = id
        }

        return id
    }

    private init() {}

}
R Menke
  • 8,183
  • 4
  • 35
  • 63
  • 1
    Why can't you expose a synchronous API which uses GCD internally to serialize access to the dictionary (using `dispatch_sync`). No completion blocks needed. – Nikolai Ruhe Nov 03 '15 at 09:13
  • 1
    You can dispatch *synchronously* to a *serial* GCD queue, compare http://stackoverflow.com/a/30853299/1187415, http://stackoverflow.com/questions/28784507/adding-items-to-swift-array-across-multiple-threads-causing-issues-because-arra, http://stackoverflow.com/questions/27839595/gcd-with-static-functions-of-a-struct. – Martin R Nov 03 '15 at 09:14
  • @MartinR how do I return from inside the dispatch_sync?? – R Menke Nov 03 '15 at 09:18
  • 1
    @RMenke: You don't. dispatch_sync() returns *after* the block has been executed (in contrast to dispatch_async()). – Martin R Nov 03 '15 at 09:19
  • @MartinR Thanks for clearing that up, again, I always forget. – R Menke Nov 03 '15 at 09:24
  • 2
    Unrelated to your question, but have a look at http://stackoverflow.com/questions/33452771/how-to-get-a-swift-type-name-as-a-string-with-its-namespace-or-framework-name for an alternative to _stdlib_getDemangledTypeName (which is not officially documented, as far as I know). – Martin R Nov 03 '15 at 09:31
  • @MartinR thx, already changed it. I just googled "get type name" and used the first thing that popped up. `reflecting` is better indeed. – R Menke Nov 03 '15 at 09:39

1 Answers1

1

Is there some way I am forgetting to make the access to the Dictionary thread safe?

Many. Using dispatch_sync on a serial queue is a good and simple way to go:

class HashID {

    static func idForType(type: Any.Type) -> Int {

        let typeName = _stdlib_getDemangledTypeName(type)
        var id : Int = 0

        dispatch_sync(queue) {

            if let currentID = storedGlobalIDForType[typeName] {
                id = currentID + 1
                storedGlobalIDForType[typeName] = id
            } else {
                storedGlobalIDForType[typeName] = 0
            }
        }

        return id
    }

    private static var queue = dispatch_queue_create("my.queue", DISPATCH_QUEUE_SERIAL)
    private static var storedGlobalIDForType : [String : Int] = [:]
}

Using a read write lock could have advantages in your case.

Community
  • 1
  • 1
Nikolai Ruhe
  • 81,520
  • 17
  • 180
  • 200
  • read write lock is interesting. I don't think it is needed. But hey, who needs to know how many instances of a type were created :) – R Menke Nov 03 '15 at 09:25
  • 1
    @RMenke A read write lock could help with reducing lock contention. If it's seldom that new types request an id (vs old types who are already in the dictionary) you can use `dispatch_barrier` to put new ids into the dict while old ids are incremented without exclusive locking. – Nikolai Ruhe Nov 03 '15 at 09:38
  • Had some coffee and doesn't it just need `dispatch_barrier_sync` since every read is also a write? It would not really be a read/write lock because there will never be just a read. – R Menke Nov 28 '15 at 14:26