skip to Main Content

I have 3 functions like this:

func getMyFirstItem(complete: @escaping (Int) -> Void) {
    DispatchQueue.main.async {
        complete(10)
    }
}

func getMySecondtItem(complete: @escaping (Int) -> Void) {
    DispatchQueue.global(qos:.background).async {
        complete(10)
    }
}

func getMyThirdItem(complete: @escaping (Int) -> Void) {
    DispatchQueue.main.async {
        complete(10)
    }
}

And I have a variable:

var myItemsTotal: Int = 0

I would like to know how to sum the items, in this case 10 10 10 to get 30. But what is the best way, since is background and main.

2

Answers


  1. I could be wrong, but I don’t think it makes much difference about the different queues,
    you still have to "wait" until
    the completions are done, for example:

    var myItemsTotal: Int = 0
    
    getMyFirstItem() { val1 in
        getMySecondtItem() { val2 in
            getMyThirdItem() { val3 in
                myItemsTotal = val1 + val2 + val3
                print(" myItemsTotal: (myItemsTotal)")
            }
        }
    }
    
    Login or Signup to reply.
  2. The key issue is to ensure thread-safety. For example, the following is not thread-safe:

    func addUpValuesNotThreadSafe() {
        var total = 0
    
        getMyFirstItem { value in
            total += value             // on main thread
        }
    
        getMySecondItem { value in
            total += value             // on some GCD worker thread!!!
        }
    
        getMyThirdItem { value in
            total += value             // on main thread
        }
    
        ...
    }
    

    One could solve this problem by not allowing these tasks run in parallel, but you lose all the benefits of asynchronous processes and the concurrency they offer.

    Needless to say, when you do allow them to run in parallel, you would likely add some mechanism (such as dispatch groups) to know when all of these asynchronous tasks are done. But I did not want to complicate this example, but rather keep our focus on the thread-safety issue. (I show how to use dispatch groups later in this answer.)

    Anyway, if you have closures called from multiple threads, you must not increment the same total without adding some synchronization. You could add synchronization with a serial dispatch queue, for example:

    func addUpValues() {
        var total = 0
        let queue = DispatchQueue(label: Bundle.main.bundleIdentifier! + ".synchronized")
    
        getMyFirstItem { value in
            queue.async {
                total += value         // on serial queue
            }
        }
    
        getMySecondItem { value in
            queue.async {
                total += value         // on serial queue
            }
        }
    
        getMyThirdItem { value in
            queue.async {
                total += value         // on serial queue
            }
        }
    
        ...
    }
    

    There are a variety of alternative synchronization mechanisms (locks, GCD reader-writer, actor, etc.). But I start with the serial queue example to observe that, actually, any serial queue would accomplish the same thing. Many use the main queue (which is a serial queue) for this sort of trivial synchronization where the performance impact is negligible, such as in this example.

    For example, one could therefore either refactor getMySecondItem to also call its completion handler on the main queue, like getMyFirstItem and getMyThirdItem already do. Or if you cannot do that, you could simply have the getMySecondItem caller dispatch the code that needs to be synchronized to the main queue:

    func addUpValues() {
        var total = 0
    
        getMyFirstItem { value in
            total += value             // on main thread
        }
    
        getMySecondItem { value in
            DispatchQueue.main.async {
                total += value         // now on main thread, too
            }
        }
    
        getMyThirdItem { value in
            total += value             // on main thread
        }
    
        // ...
    }
    

    That is also thread-safe. This is why many libraries will ensure that all of their completion handlers are called on the main thread, as it minimizes the amount of time the app developer needs to manually synchronize values.


    While I have illustrated the use of serial dispatch queues for synchronization, there are a multitude of alternatives. E.g., one might use locks or GCD reader-writer pattern.

    The key is that one should never mutate a variable from multiple threads without some synchronization.


    Above I mention that you need to know when the three asynchronous tasks are done. You can use a DispatchGroup, e.g.:

    func addUpValues(complete: @escaping (Int) -> Void) {
        let total = Synchronized(0)
        let group = DispatchGroup()
    
        group.enter()
        getMyFirstItem { first in
            total.synchronized { value in
                value += first
            }
            group.leave()
        }
    
        group.enter()
        getMySecondItem { second in
            total.synchronized { value in
                value += second
            }
            group.leave()
        }
    
        group.enter()
        getMyThirdItem { third in
            total.synchronized { value in
                value += third
            }
            group.leave()
        }
    
        group.notify(queue: .main) {
            let value = total.synchronized { $0 }
            complete(value)
        }
    }
    

    And in this example, I abstracted the synchronization details out of addUpValues:

    class Synchronized<T> {
        private var value: T
        private let lock = NSLock()
    
        init(_ value: T) {
            self.value = value
        }
    
        func synchronized<U>(block: (inout T) throws -> U) rethrows -> U {
            lock.lock()
            defer { lock.unlock() }
            return try block(&value)
        }
    }
    

    Obviously, use whatever synchronization mechanism you want (e.g., GCD or os_unfair_lock or whatever).

    But the idea is that in the GCD world, dispatch groups can notify you when a series of asynchronous tasks are done.


    I know that this was a GCD question, but for the sake of completeness, the Swift concurrency asyncawait pattern renders much of this moot.

    func getMyFirstItem() async -> Int {
        return 10
    }
    
    func getMySecondItem() async -> Int {
        await Task.detached(priority: .background) {
            return 10
        }.value
    }
    
    func getMyThirdItem() async -> Int {
        return 10
    }
    
    func addUpValues() {
        Task {
            async let value1 = getMyFirstItem()
            async let value2 = getMySecondItem()
            async let value3 = getMyThirdItem()
            let total = await value1 + value2 + value3
            print(total)
        }
    }
    

    Or, if your async methods were updating some shared property, you would use an actor to synchronize access. See Protect mutable state with Swift actors.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search