Home > Net >  calling two functions after each other using DispatchQueue / DispatchGroup
calling two functions after each other using DispatchQueue / DispatchGroup

Time:12-22

TL;DR see below for a code example of the problem.

I have two functions async update() that updates some data on the server and waits for the response. I have the option to provide a callback or make the function async and wait for the response. The second function fetch() lives in a different class (actually a different @StateObject, i.e. ObservableObject class - I'm using SwiftUI).

The function fetch() is triggered from a view, but it should "wait" for the update() function to have finished (if there is one running) before executing.

I want to keep the two class instances -where these functions are part of- separate. Thus I cannot explicitly use callback or alike. After searching and discovering DispatchQueue, DispatchGroup, Task, OperationQueue and concepts of synchronous/asynchronous, serial/concurrent I am quite confused on what to use since many of them seem to be very similar tools.

  1. DispatchQueue (& Task)

    At first I thought DispatchQueue was the way to go since the docs seem to describe exactly what I need: "... Dispatch queues execute tasks either serially or concurrently. ..." The DispatchQueue.main is serial so I thought simply executing both function calls like this would wait for the update() to finish if there was one started anywhere in my app and fetch() would run afterwards. But apparently:

    DispatchQueue.main.async {
        self.example = await self.update()
    }
    

    That throws an error:

    Cannot pass function of type '() async -> ()' to parameter expecting synchronous function type

    That already seems weird to me because of its name DispatchQueue.main.async. I can avoid this by wrapping the call in a Task {} but then I suppose it gets run on a different thread, as it did not finish the Task before running the fetch() function. Thus it was no longer serial:

    DispatchQueue.main.async {
        Task {
            print("1")
            try! await Task.sleep(nanoseconds: 10_000_000_000)
            print("2")
        }
        Task {
            print("3")
        }
    }
    
    // prints 1,3, ..., 2
    

    Using DispatchQueue.main.sync could seem more of what I need according to this, but I get the error:

    No exact matches in call to instance method 'sync'

    Is there a way to use DispatchQueue to accomplish my goal? (I also tried creating my own queue with the same result as using the global main queue. I would want to use an asynchronous function and block any other function on this queue until after finishing execution)

  2. DispatchGroup

    Next I tried using DispatchGroup as shown here but was already thrown off by having to pass around this DispatchGroup() instance to reference in both classes. Is it the only way to accomplish my goal? Could I avoid passing one single object to both classes at initalisation?

  3. OperationQueue

    After reading further I stumbled across OperationQueue here. Again, this seems to solve my issue, but I once again have to pass this OperationQueue object to both classes.

Can someone explain the distinguishing differences of these approaches in relation to my problem? Which one is the "proper" way to do it? I would assume not having to pass around some objects is easier, but then how can I use a global DispatchQueue to serially execute some async functions?


Here a MRE, after clicking "start long task" and right away fetch the console should read: "1","2","these would be some results"

import SwiftUI

class Example1ManagerState: ObservableObject {
    func update() async {
        print("1")
        try! await Task.sleep(nanoseconds: 10_000_000_000)
        print("2")
    }
}

class Example2ManagerState: ObservableObject {
    func fetch() {
        print("these would be some results")
    }
}

struct ContentView2: View {
    
    @StateObject var ex1 = Example1ManagerState()
    @StateObject var ex2 = Example2ManagerState()
    
    var body: some View {
        Button {
            Task {
                await ex1.update()
            }
        } label: {
            Text("start long task")
        }
        
        Button {
            ex2.fetch()
        } label: {
            Text("fetch")
        }
    }
}

CodePudding user response:

According to your example you just need to add one line

    Button {
        Task {
            await ex1.update()
            ex2.fetch() // here this line doesn’t run unless update is done
        }
    } label: {
        Text("start long task")
    }

CodePudding user response:

Swift concurrency is well suited for handling dependencies between tasks that are, themselves, asynchronous. We could wrap the first async function call in a Task, and then later await the result of that Task:

struct ContentView: View {
    @StateObject var viewModel = ViewModel()

    var body: some View{
        VStack{
            Text(viewModel.results)
            Button("Example 1 - update") { viewModel.update() }
            Button("Example 2 - fetch")  { viewModel.fetch() }
        }
    }
}

@MainActor
class ViewModel: ObservableObject {
    @Published var results = ""

    var task: Task<Void, Never>?

    let example1 = Example1ManagerState()
    let example2 = Example2ManagerState()

    func update() {
        task = Task {
            results = "starting update"
            let updateResults = await example1.update()
            results = updateResults
        }
    }

    func fetch() {
        Task {
            _ = await task?.result
            results = "starting fetch"
            let fetchResults = await example2.fetch()
            results = fetchResults
        }
    }
}

actor Example1ManagerState {
    func update() async -> String {
        let seconds: TimeInterval = .random(in: 2...3)
        try? await Task.sleep(for: .seconds(seconds))
        return "\(#function) finished \(seconds)"
    }
}

actor Example2ManagerState {
    func fetch() async -> String {
        let seconds: TimeInterval = .random(in: 2...3)
        try? await Task.sleep(for: .seconds(seconds))
        return "\(#function) finished \(seconds)"
    }
}

Above, I have abstracted the business logic out of the “view” and into a “view model” (with the goal of a better separation of responsibilities, which simplifies unit testing, etc.), but that is unrelated to the broader point. The observation is that whoever is calling update and fetch should manage the dependencies between these tasks (in this case, the view model has the Task property), and that this logic not be incorporated in Example1ManagerState or Example2ManagerState.


A few other random observations:

  1. We should generally avoid intermingling GCD API with Swift concurrency’s async-await. I would advise against using DispatchQueue.main.async {…} in Swift concurrency code. In general, this is a bad practice, and in some cases, it can cause serious problems. The compiler is getting better about warning us of this misuse, and is likely to be a hard error in Swift 6.

  2. If you are not using Swift concurrency and are considering legacy approaches, a few observations regarding points in your question:

    • DispatchQueue is excellent for managing blocks of code added to a queue. And you can dispatch these blocks of code either synchronously (sync) or asynchronously (async). That synchronous/asynchronous dispatch dictates whether the caller’s thread will be blocked while the dispatched block runs.

      But DispatchQueue is only useful if the code inside the dispatched block, itself, runs synchronously. Do not conflate the synchronous/asynchronous nature with respect to the caller (namely, does the caller wait or not) with the synchronous/asynchonous nature of the code inside the dispatched block.

      For example, imagine that you dispatched a URLSession dataTask to a dispatch queue. That queue will manage the creation of those URLSession requests, but not the waiting for those asynchronous requests to actually finish. (Lol.)

      In general, avoid using dispatch queues for launching tasks that are, themselves, asynchronous.

    • To solve this problem in legacy codebases, we would turn to operation queues for managing dependencies between tasks that are, themselves, asynchronous. It managed dependencies between asynchronous operations exceptionally well. (When implemented well, it also features excellent separation of responsibilities affording us highly-cohesive, loosely-coupled code.)

      But writing asynchronous Operation subclasses is extremely fiddly. Once you set it up, it is a wonderful pattern, but the proper implementation of a custom asynchronous subclass of the Operation object is far from intuitive or obvious. E.g., https://stackoverflow.com/a/48104095/1271826.)

      Swift concurrency, with async-await, eliminates all of that ugliness.

    • You mention DispatchGroup. That is really intended for establishing dependencies between multiple GCD work items. (E.g., you might want to trigger block of code after a bunch of other dispatched blocks, running in parallel, finish.)

      Because you can manually enter and leave dispatch groups, we can contort ourselves to use this to manage dependencies between blocks of code that are, themselves, asynchronous, but again, Swift concurrency handles this far more gracefully.

  3. Regarding your question about not wanting to pass the legacy object managing the dependencies (i.e., the operation queue, or the dispatch group, or whatever), you are absolutely correct: You would want to avoid that.

    For example, in the operation queue world, rather than passing the operation queue to this other manager object, you would instead refactor the code so that the manager object could vend an operation, and let the caller add it to its own operation queue. That avoids passing the queue about.

    The same is true for the dispatch groups. Rather than passing the DispatchGroup object around (at which point it becomes very difficult to diagnose problems, understand dependencies, etc.), you would give the two ManagerState methods a completion handler, and the caller would enter the group before calling the method and leave in the completion handler.

    Bottom line, yes, avoid passing operation queues or dispatch groups or what-have-you around. Just like I have isolated the Task dependency in the calling code (the view model in my example), you would do the same in traditional sorts of patterns, avoiding the entangling of dependencies in these manager types, altogether, and letting the caller handle this.

In short, Swift concurrency should do what you need, and get you out of the complexities of these brittle and complicated GCD implementations.

CodePudding user response:

There are a few different options you can consider to achieve your goal of having the fetch function wait for the update function to finish before executing. Here are some options and their differences:

DispatchQueue: A DispatchQueue is a way to execute tasks either serially (one at a time) or concurrently (at the same time). You can use a serial DispatchQueue to ensure that tasks are executed one at a time in the order they are added to the queue. To use a serial DispatchQueue, you can create your own DispatchQueue and set the attribute .isSerial to true. Alternatively, you can use the .main queue, which is a serial queue that is used to execute tasks on the main thread. However, you cannot use the await keyword with a DispatchQueue, as it is not aware of async/await. Instead, you can use a closure or a function as a task to be executed on the queue.

DispatchGroup: A DispatchGroup allows you to group multiple tasks together and wait for all of them to finish before moving on to the next set of tasks. You can use a DispatchGroup to ensure that the fetch function waits for the update function to finish before executing. To do this, you can add the update function to the DispatchGroup before executing it, and then call DispatchGroup.wait() in the fetch function to wait for the update function to finish before continuing. You will need to pass a reference to the DispatchGroup object to both classes in order to use it.

  • Related