Search code examples
swiftgrand-central-dispatchdispatchgroup

Does a dispatch group block the main thread in iOS?


I'm calling some code to query and update the CallKit call extension, and very occassionally I've seen a hang and the call is timing out. I thought I'd put this off the main thread so that it wouldn't be blocking the UI, but is that actually not the case?

The UI is blocking up, I don't know if its this code below, or something else.

Here's the query function:

    private func queryCallExtensionStatusWithDispatchGroup() {
        let dg = DispatchGroup()
        dg.enter()
        self.doQueryCallExtensionStatusWithDispatchGroup(dispatchGroup: dg)
        Logger.trace(TAG + "queryCallExtensionStatusWithDispatchGroup() WAITING .... ")
        let dispatchTimeoutResult = dg.wait(timeout:  DispatchTime.now() + DispatchTimeInterval.seconds(30))
        if dispatchTimeoutResult == .timedOut {
            Logger.info(TAG + "queryCallExtensionStatusWithDispatchGroup() TIMED OUT")
        } else {
            Logger.trace(TAG + "queryCallExtensionStatusWithDispatchGroup() LEFT")
        }
    }


private func doQueryCallExtensionStatusWithDispatchGroup(dispatchGroup:DispatchGroup)  {
<snip>
    dispatchGroup.leave() 
}

And its invocation:

private let callExtensionQueue = DispatchQueue(label: "com.appname.serialQueue")

...
callExtensionQueue.async {
    self!.queryCallExtensionStatusWithDispatchGroup()
}

Solution

  • Your code snippet will block the worker thread used by callExtensionQueue, not the main thread.

    So, unless you have some sync dispatches to callExtensionQueue elsewhere (or some unusual thread explosion scenario), the above will not block the main thread.

    Your problem likely rests elsewhere.


    In cases where you want to make sure you do not accidentally call a function from the main queue, you might add a dispatchPrecondition, so you will get a warning in debug builds if you accidentally call this function from the main queue:

    private func queryCallExtensionStatusWithDispatchGroup() {
        dispatchPrecondition(condition: .notOnQueue(.main))
    
        let group = DispatchGroup()
        group.enter()
        doQueryCallExtensionStatus {
            group.leave()
        }
        logger.trace("\(#function) WAITING .... ")
        let result = group.wait(timeout: .now() + .seconds(30))
        if result == .timedOut {
            logger.info("\(#function) TIMED OUT")
        } else {
            logger.trace("\(#function) LEFT")
        }
    }
    

    As an aside, in the above, rather than passing the dispatch group to the called function (unnecessarily entangling these two functions), the standard technique is a completion handler closure parameter (and you can make it optional, if you want):

    private func doQueryCallExtensionStatus(completion: (() -> Void)? = nil) {
        …
        completion?()
    }
    

    That way, all of the DispatchGroup related code is isolated to a single function, queryCallExtensionStatusWithDispatchGroup.