Search code examples
iosswiftasynchronousgrand-central-dispatch

Weak DispatchGroup in closures and other GCD questions


Swift closures strongly capture reference types.

DispatchGroup is a reference type.

My questions have to do with the following code:

func getUsername(onDone: @escaping (_ possUsername: String?) -> ())
{
    //Post request for username that calls onDone(retrievedUsername)...
}

func getBirthdate(using username: String?, onDone: @escaping (_ possBday: String?) -> ())
{
    //Post request for token that calls onDone(retrievedToken)...
}

func asyncTasksInOrder(onDone: @escaping (_ resultBDay: String?) -> ())
{
    let thread = DispatchQueue(label: "my thread", qos: .userInteractive, attributes: [],
                               autoreleaseFrequency: .workItem, target: nil)
    thread.async { [weak self, onDone] in
        guard let self = self else {
            onDone(nil)
            return
        }
        let dg = DispatchGroup()        //This is a reference type
        var retrievedUsername: String?
        var retrievedBday: String?

        //Get username async first
        dg.enter()
        self.getUsername(onDone: {[weak dg](possUsername) in
            retrievedUsername = possUsername
            dg?.leave() //DG is weak here
        })
        dg.wait()

        //Now that we've waited for the username, get bday async now
        dg.enter()
        self.getBirthdate(using: retrievedUsername, onDone: {[weak dg](possBday) in
            retrievedBday = possBday
            dg?.leave() //DG is also weak here
        })
        dg.wait()

        //We've waited for everything, so now call the return callback
        onDone(retrievedBday)
    }
}

So the two closures inside of asyncTasksInOrder(onDone:) each capture dg, my DispatchGroup.

  1. Is it even necessary to capture my dispatch group?
  2. If I don't capture it, how would I even know I've got a retain cycle?
  3. What if the dispatch group evaporates during one of the callback executions? Would it even evaporate since it's waiting?
  4. Is it unnecessarily expensive to instantiate a DispatchQueue like this often (disregarding the .userInteractive)? I'm asking this particular question because spinning up threads in Android is extremely expensive (so expensive that JetBrains has dedicated lots of resources towards Kotlin coroutines).
  5. How does dg.notify(...) play into all of this? Why even have a notify method when dg.wait() does the same thing?

I feel like my understanding of GCD is not bulletproof, so I'm asking to build some confidence. Please critique as well if there's anything to critique. The help is truly appreciated.


Solution

  • 1) No, the dispatch group is captured implicitly. You don't even need to capture self in async because GCD closures don't cause retain cycles.

    2) There is no retain cycle.

    3) Actually you are misusing DispatchGroup to force an asynchronous task to become synchronous.

    4) No, GCD is pretty lightweight.

    5) The main purpose of DispatchGroup is to notify when all asynchronous tasks – for example in a repeat loop – are completed regardless of the order.


    A better solution is to nest the asynchronous tasks. With only two tasks the pyramid of doom is manageable.

    func asyncTasksInOrder(onDone: @escaping (String?) -> Void)
    {
        let thread = DispatchQueue(label: "my thread", qos: .userInteractive, autoreleaseFrequency: .workItem)
        thread.async {       
    
            //Get username async first
            self.getUsername { [weak self] possUsername in
                guard let self = self else { onDone(nil); return }
    
                //Now get bday async
                self.getBirthdate(using: possUsername) { possBday in
    
                   //Now call the return callback
                    onDone(possBday)
                } 
            }
        }
    }