Good alternatives to DispatchGroup for repeating events?

Here is my scenario: I want to execute a block of code after several asynchronous tasks are completed. DispatchGroup is usually a good tool for this- but it has one massive limitation that absolutely breaks my heart.

Some of my work items run more than once in the program lifecycle, and I simply want to know when each has finished their first execution. Unfortunately, DisptachGroup doesn't work for this because the leave calls must be balanced with the enter calls. You cannot leave more than you enter or the program will crash.

What would be some good thread-safe alternatives to DispatchGroup for awaiting the first execution of multiple repeating async blocks? In the past I have used DispatchSemaphores to augment the DispatchGroup, and it works, but it's hardly elegant.

Ideally, if I could get my way, I would love it if you could assign a string label to each enter call, and then leave as many times as you want with that label. Example:

let myDispatchGroup = DispatchGroup()

myDispatchGroup.enter("task_foo")
someFunctionWithAsyncCompletionBlock {
    defer { myDispatchGroup.leave("task_foo") }
    // do stuff once
}

myDispatchGroup.enter("task_bar")
someFunctionWithRepeatingAsyncCompletionBlock {
    defer { myDispatchGroup.leave("task_bar") }
    // do stuff multiple times
}

myDispatchGroup.enter("task_baz")
anotherFunctionWithRepeatingAsyncCompletionBlock {
    defer { myDispatchGroup.leave("task_baz") }
    // do stuff multiple times
}

myDispatchGroup.notify(queue: .main, work: DispatchWorkItem(block: {
    // Carry on after task_foo, first task_bar, and first task_baz
}))

I’m not sure I understand the problem you’re getting at here. Can you expand your example a little to explain what you mean by “do stuff multiple times”?

My best guess is illustrated by the program at the end of this post. It prints:

Hello
Cruel
World!
done

My understanding is that you want it to print:

Hello
done
Cruel
World!

You can do that by tweaking the printWork(…) function as follows:

func printWork(_ work: ArraySlice<String>, notify group: DispatchGroup?) {
    guard let first = work.first else {
        return
    }
    printAsync(first) {
        printWork(work.dropFirst(), notify: nil)
    }
    if let group = group {
        group.leave()
    }
}

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple


import Dispatch

func printAsync(_ str: String, completionHandler: @escaping () -> Void) {
    print(str)
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
        completionHandler()
    }
}

func printWork(_ work: ArraySlice<String>, notify group: DispatchGroup) {
    guard let first = work.first else {
        group.leave()
        return
    }
    printAsync(first) {
        printWork(work.dropFirst(), notify: group)
    }
}

func main() {
    let group = DispatchGroup()

    let work = ["Hello", "Cruel", "World!"]
    group.enter()
    printWork(work[...], notify: group)
    group.notify(queue: .main) {
        print("done")
    }

    dispatchMain()
}

main()
1 Like

Sure, let me expand using your example output. This is what I am trying to achieve:

Hello
Cruel
Cruel
World!
done
Cruel
World!
World!
World!
Cruel

I want to wait for each task to have finished once- but some tasks will fire multiple times over the course of the program lifecycle.

Could you have a dictionary of the task ID strings with the values being incremented every time they fire. Then you could check if any of the values was 0?

Yes, you could probably get away with that, but the only way I can think of making your idea event-driven is to call the done block within each task item- and then checking the dictionary within the done block- but I fear thread-safety or race condition problems arising from that- unless maybe I force everything onto a DispatchQueue.

Hmmm. Either I’ve misunderstood you or you’ve misunderstood me (-: In my example, the printWork(…) mechanism was meant to be equivalent to one of your task_xxx items. So to get the effect you’re looking for you’d have printWorkFoo, printWorkBar and printWorkBaz. At that that point you’ll get a sequence like:

foo-Hello
bar-Hello
baz-Hello
done
foo-Cruel
bar-Cruel
…

which seems to be what you’re looking for.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

If I'm not mistaken (and maybe I am), you're sequence has done immediately follow Hello, which is not what I want. I do not want done to fire before Hello AND Cruel AND World! have fired at least once each, much like the default DispatchGroup behavior. The reason I can't just use a normal DG is because the Cruel callback block can be fired multiple times.

Let me offer a more real example drawn from an iOS app I'm working on. At session start in my root UIViewController, I need to wait for several things to setup before performing an action.

  1. CLLocationManager authorization status must change to any state other than notDetermined.
  2. Firebase authentication state listener must fire
  3. Firebase Firestore listener must be attached and have received the initial snapshot

All three of these items must complete before I proceed with a specific block of code.

The reason I can't simply use a normal DispatchGroup is because all three of those items can re-fire at any time. If I put myDispatchGroup.leave() inside of the locationManager didChangeAuthorizationCallback delegate function, then the second time it fires the app will crash. Same deal with the authentication and database listener. Both of those can fire again after their initialization.

I'm only calling each of these tasks once- the repetition comes from these "listener" closures that can be run again and again by the object being listened to. I need an elegant way of awaiting the first execution of each closure.

Maybe I'm not understanding you correctly. If so, please let me know! :)

I do not want done to fire before Hello AND Cruel AND World! have
fired at least once each, much like the default DispatchGroup
behavior.

I think you’ve misunderstood what I’m saying above. Consider Example 1, below. It prints:

Pompey
Huey
Alice
done
Crassus
Dewey
Bob
Caesar
Louie
Mallory

That is, it prints done after the first item in each work item list is done.


However, your more concrete example makes it clear that I’m solving the wrong problem (-: There’s a bunch of different ways you could solve your problem but Example 2, also below, stays within the Dispatch world.

Note In this code I’m using timers, locationTimer and so on, to simulate your various event sources.

This code uses mutable state, locationGroup and so on, which can be problematic in concurrent code, but that state is very tightly confined.

Another option is to use the much-neglected DispatchSourceUserDataOr. See Example 3. I kinda like this approach because the OR construct matches your overall semantics.

Share and Enjoy

Quinn “The Eskimo!” @ DTS @ Apple

Example 1
import Dispatch

func printAsync(_ str: String, completionHandler: @escaping () -> Void) {
    print(str)
    DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
        completionHandler()
    }
}

func printWork(_ work: ArraySlice<String>, notify group: DispatchGroup?) {
    guard let first = work.first else {
        return
    }
    printAsync(first) {
        printWork(work.dropFirst(), notify: nil)
    }
    if let group = group {
        group.leave()
    }
}

func main() {
    let group = DispatchGroup()

    group.enter()
    printWork(["Pompey", "Crassus", "Caesar"][...], notify: group)

    group.enter()
    printWork(["Huey", "Dewey", "Louie"][...], notify: group)

    group.enter()
    printWork(["Alice", "Bob", "Mallory"][...], notify: group)

    group.notify(queue: .main) {
        print("done")
    }

    dispatchMain()
}

main()
Example 2
import Dispatch

let locationTimer = DispatchSource.makeTimerSource(queue: .main)

func startLocation(notify group: DispatchGroup) {
    group.enter()
    locationTimer.schedule(deadline: .now() + 0.1, repeating: 0.5)
    var locationGroup: DispatchGroup? = group
    locationTimer.setEventHandler {
        print("did change location")
        if let group = locationGroup {
            locationGroup = nil
            group.leave()
        }
    }
    locationTimer.activate()
}

let authTimer = DispatchSource.makeTimerSource(queue: .main)

func startAuth(notify group: DispatchGroup) {
    group.enter()
    authTimer.schedule(deadline: .now() + 0.1, repeating: 1.0)
    var authGroup: DispatchGroup? = group
    authTimer.setEventHandler {
        print("did change auth state")
        if let group = authGroup {
            authGroup = nil
            group.leave()
        }
    }
    authTimer.activate()
}

let listenerTimer = DispatchSource.makeTimerSource(queue: .main)

func startListener(notify group: DispatchGroup) {
    group.enter()
    listenerTimer.schedule(deadline: .now() + 0.1, repeating: 1.5)
    var listenerGroup: DispatchGroup? = group
    listenerTimer.setEventHandler {
        print("did attach listener")
        if let group = listenerGroup {
            listenerGroup = nil
            group.leave()
        }
    }
    listenerTimer.activate()
}

func main() {
    let group = DispatchGroup()

    startLocation(notify: group)
    startAuth(notify: group)
    startListener(notify: group)

    group.notify(queue: .main) {
        print("ready")
    }

    dispatchMain()
}

main()
Example 2
import Dispatch

let locationTimer = DispatchSource.makeTimerSource(queue: .main)

func startLocation(notify source: DispatchSourceUserDataOr) {
    locationTimer.schedule(deadline: .now() + 0.1, repeating: 0.5)
    locationTimer.setEventHandler {
        print("did change location")
        source.or(data: 0x01)
    }
    locationTimer.activate()
}

let authTimer = DispatchSource.makeTimerSource(queue: .main)

func startAuth(notify source: DispatchSourceUserDataOr) {
    authTimer.schedule(deadline: .now() + 0.1, repeating: 1.0)
    authTimer.setEventHandler {
        print("did change auth state")
        source.or(data: 0x02)
    }
    authTimer.activate()
}

let listenerTimer = DispatchSource.makeTimerSource(queue: .main)

func startListener(notify source: DispatchSourceUserDataOr) {
    listenerTimer.schedule(deadline: .now() + 0.1, repeating: 1.5)
    listenerTimer.setEventHandler {
        print("did attach listener")
        source.or(data: 0x04)
    }
    listenerTimer.activate()
}

func main() {
    let source = DispatchSource.makeUserDataOrSource(queue: .main)
    var received: UInt = 0
    source.setEventHandler {
        guard received != 0x07 else { return }
        received |= source.data
        if received == 0x07 {
            print("ready")
        }
    }
    source.activate()

    startLocation(notify: source)
    startAuth(notify: source)
    startListener(notify: source)

    dispatchMain()
}

main()
1 Like

I think the 3rd example is the best, especially after you glorify it with OptionSet and per-option event handlers.

I have not seen DispatchSemaphores being used. I am getting crashes when using Dispatch Groups. Can anyone provide an example of using best way to handle repeating async calls.

Terms of Service

Privacy Policy

Cookie Policy