user2727195
user2727195

Reputation: 7330

Thread safe access to a variable in a class

in an application where there could be multiple threads running, and not sure about the possibilities if these methods will be accessed under a multhreaded environment or not but to be safe, I've done a test class to demonstrate a situation.

One method has was programmed to be thread safe (please also comment if it's done right) but the rest were not.

In a situation like this, where there is only one single line of code inside remove and add, is it necessary to make them thread safe or is it going to be exaggeration.

import Foundation

class Some {}

class Test {
    var dict = [String: Some]()

    func has(key: String) -> Bool {
        var has = false
        dispatch_sync(dispatch_queue_create("has", nil), { [unowned self] in
            has = self.dict[key] != nil
        })
        return has
    }

    func remove(key: String) -> Some {
        var ob = dict[key]
        dict[key] = nil
        return ob
    }

    func add(key: String, ob: Some) {
        dict[key] = ob
    }
}

Edit after comments

class Some {}

class Test {
    var dict = [String: Some]()
    private let queue: dispatch_queue_t = dispatch_queue_create("has", DISPATCH_QUEUE_CONCURRENT)

    func has(key: String) -> Bool {
        var has = false
        dispatch_sync(queue) {
            has = self.dict[key] != nil
        }
        return has
    }

    func remove(key: String) -> Some? { //returns
        var removed: Some?
        dispatch_barrier_sync(queue) {
            removed = self.dict.removeValueForKey(key)
        }
        return removed
    }

    func add(key: String, ob: Some) { //not async
        dispatch_barrier_sync(queue) {
            self.dict[key] = ob
        }
    }
}

Upvotes: 4

Views: 4151

Answers (2)

Abizern
Abizern

Reputation: 150615

The way you are checking whether a key exists is incorrect. You are creating a new queue every time, which means the operations are not happening synchronously.

The way I would do it is like so:

class Some {}

class Test {
    var dict = [String: Some]()
    private let queue: dispatch_queue_t = dispatch_queue_create("has", DISPATCH_QUEUE_CONCURRENT)

    func has(key: String) -> Bool {
        var has = false
        dispatch_sync(queue) { [weak self] in
            guard let strongSelf = self else { return }

            has = strongSelf.dict[key] != nil
        }

        return has
    }

    func remove(key: String) {
        dispatch_barrier_async(queue) { [weak self] in
            guard let strongSelf = self else { return }

            strongSelf.dict[key] = nil
        }
    }

    func add(key: String, ob: Some) {
        dispatch_barrier_async(queue) { [weak self] in
            guard let strongSelf = self else { return }

            strongSelf.dict[key] = ob
        }
    }
}

Firstly, I am creating a serial queue that is going to be used to access the dictionary as a property of the object, rather than creating a new one every time. The queue is private as it is only used internally.

When I want to get a value out of the class, I am just dispatching a block synchronously to the queue and waits for the block to finish before returning whether or not the queue exists. Since this is not mutating the dictionary, it is safe for multiple blocks of this sort to run on the concurrent queue.

When I want to add or remove values from the dictionary, I am adding the block to the queue but with a barrier. What this does is that it stops all other blocks on the queue while it is running. When it is finished, all the other blocks can run concurrently. I am using an async dispatch, because I don't need to wait for a return value.

Imagine you have multiple threads trying to see whether or not key values exist or adding or removing values. If you have lots of reads, then they happen concurrently, but when one of the blocks is run that will change the dictionary, all other blocks wait until this change is completed and then start running again.

In this way, you have the speed and convenience of running concurrently when getting values, and the thread safety of blocking while the dictionary is being mutated.

Edited to add

self is marked as weak in the block so that it doesn't create a reference cycle. As @MartinR mentioned in the comments; it is possible that the object is deallocated while blocks are still in the queue, If this happens then self is undefined, and you'll probably get a runtime error trying to access the dictionary, as it may also be deallocated.

By setting declaring self within the block to be weak, if the object exists, then self will not be nil, and can be conditionally unwrapped into strongSelf which points to self and also creates a strong reference, so that self will not be deallocated while the instructions in the block are carried out. When these instructions complete, strongSelf will go out of scope and release the strong reference to self.

This is sometimes known as the "strong self, weak self dance".

Edited Again : Swift 3 version

class Some {}

class Test {
    var dict = [String: Some]()
    private let queue = DispatchQueue(label: "has", qos: .default, attributes: .concurrent)

    func has(key: String) -> Bool {
        var has = false
        queue.sync { [weak self] in
            guard let strongSelf = self else { return }

            has = strongSelf.dict[key] != nil
        }

        return has
    }

    func remove(key: String) {
        queue.async(flags: .barrier) { [weak self] in
            guard let strongSelf = self else { return }

            strongSelf.dict[key] = nil
        }
    }

    func add(key: String, ob: Some) {
        queue.async(flags: .barrier) { [weak self] in
            guard let strongSelf = self else { return }

            strongSelf.dict[key] = ob
        }
    }
}

Upvotes: 7

David Rysanek
David Rysanek

Reputation: 979

Here is another swift 3 solution which provides thread-safe access to AnyObject.

It allocates recursive pthread_mutex associated with 'object' if needed.

class LatencyManager
{
    private var latencies = [String : TimeInterval]()

    func set(hostName: String, latency: TimeInterval) {
        synchronizedBlock(lockedObject: latencies as AnyObject) { [weak self] in
            self?.latencies[hostName] = latency
        }
    }

    /// Provides thread-safe access to given object
    private func synchronizedBlock(lockedObject: AnyObject, block: () -> Void) {
        objc_sync_enter(lockedObject)
        block()
        objc_sync_exit(lockedObject)
    }
}

Then you can call for example set(hostName: "stackoverflow.com", latency: 1)

UPDATE

You can simply define a method in a swift file (not in a class):

/// Provides thread-safe access to given object
public func synchronizedAccess(to object: AnyObject, _ block: () -> Void)
{
    objc_sync_enter(object)
    block()
    objc_sync_exit(object)
}

And use it like this:

synchronizedAccess(to: myObject) {
    myObject.foo()
 }

Upvotes: 0

Related Questions