Run Async task

Hi,

Overview (Edited):

  • I have a macOS command line app (CLI)
  • I would like to run some asynchronous task inside run.
  • Normally the run method would exit after the last line inside it is executed.
  • For async task I wanted it not to exit and so was using dispatchMain()
  • I have pasted below my attempt at it using dispatchMain() and exit(EXIT_SUCCESS).

Question (Edited):

Is using dispatchMain() a good approach or is there a better pattern / approach to do it?

Code:

import Foundation
import ArgumentParser

struct Example : ParsableCommand {
    
    func run() throws {
        
        print("run started")
        
        let model = Model()
        
        model.asyncTask()
        
        //To prevent it from exiting
        dispatchMain()
    }
}

class Model {
    
    func asyncTask() {
        
        DispatchQueue.main.async {
            
            for index in 1...1000 {
                print(index)
            }
            
            //Exit after completion of async task
            exit(EXIT_SUCCESS)
        }
    }
}

Example.main()
1 Like

One way to solve this is

struct Example : ParsableCommand {
    func run() throws {
        print("run started")
        let model = Model()
        let group = DispatchGroup()
        model.asyncTask(group: group)
        //Exit after completion of async task:
        group.notify {
            exit(EXIT_SUCCESS)
        }
        dispatchMain()
    }
}

class Model {
    func asyncTask(group: DispatchGroup) {
        DispatchQueue.main.async(group: group) {
            for index in 1...1000 {
                print(index)
            }
        }
    }
}

Example.main()

Note how you could even star additional async blocks, as long as they use the group.

There are other ways to do this, but they all follow this general pattern.

1 Like

@deggert Thanks a lot for that example, dispatch group seems like a good option.

I suppose the general pattern is to use dispatchMain() and exit(EXIT_SUCCESS) .

I'm unclear what the context is of your question. Is this in a GUI app or command line? How much work do these tasks do? milliseconds or minutes or unknown? Do they need to be cancelable?

For tasks that may take a while you don't want to run them on the main thread, like your code is doing. I would never call exit() from inside DispatchQueue.main.async. The code just falls off the end.

For tasks that may take a while I'd use NSOperation or OperationQueue.addOperation() to add closures to the Queue.

1 Like

Thanks @phoneyDev, I am using Operation (NSOperation) as I have dependencies and want them to be cancelled.

My bad, my question wasn't very well written. Normally the run method would exit after the last line inside it is executed. For async task I wanted it not to exit and so was using dispatchMain(). I wanted to know if that was a good approach or if there is a better approach for it.

The main queue is only special because, for GUI apps, that’s where the UI runs. So if you’re doing heavy processing on it, the UI becomes unresponsive because other work is being done. You can offload that work on to another queue to allow the UI to keep responding to user input while other things are being processed.

For CLI apps, that’s only relevant if you want to accept commands from the terminal as the app is executing.

Otherwise, all the code here is totally fine. Using dispatchMain is correct, and ending the program with exit is correct. How you structure it - by using a DispatchGroup callback or just calling exit directly somewhere - is up to you, and depends on the complexity of your app. As long as you can clearly understand when the app will stop executing, and (very important) you can ensure that it indeed always stops, choose whichever style you prefer.

2 Likes

Thanks a lot @Karl for clearly explaining it.

My Use case:

  • It is a macOS command line app (CLI).
  • It takes some inputs, then does some async processing, then shows the output or throws an error.
  • While processing, the app blocks the user from doing anything else.

Questions:

  • Pardon my ignorance is it normal for command line apps to block the user doing anything else during processing ?
  • Are there use cases / examples when command line apps would accept input while processing ?

In that case, it might not be worth making things async. Asynchronicity tends to add complexity to your code.

That is very much the norm, yes; users can always quit applications with CTRL+C, or suspend them with CTRL+Z, and the shell environment provides ways to resume suspended "jobs" in the background (see bg, particularly the "Application usage" section, and the related jobs command).

Also yes. For example, I was recently setting up some bluetooth devices on a linux machine using the bluetoothctl command. It has its own mini-prompt system, and is able to scan for devices and report changes while you type additional commands.

So for example, you'll enter scan on, and it will monitor for BT devices in the background, then you'll take one of the MAC addresses it reports and enter pair 01:23:45:67:89. That kind of interface requires that the prompt remains available for user input while some background service is running.

In bluetoothctl's case, I think it's just getting callbacks from some system service, so it's that service which is running asynchronously. Consider the alternative - if it did some kind of blocking "scan" call, then the app would freeze and not accept user input until the system decided it wanted to return control to the prompt (or until one of the callbacks stopped the scan operation). Like this, it can scan continuously, and the user can enter commands whenever they want.

1 Like

@Karl, thank you so much for patiently explaining !!

Bluetooth scanning is a nice example to allow the user to input while processing.

In my case there are multiple steps and some of them are async APIs. They all print to the console.

Since there are multiple steps and they have dependancies, I felt it would be good to have them as Operations (NSOperation) so that error handling / cancellation is easier and to avoid nested closures.

When all the steps complete or when an error is encountered I cancel the remaining operations and call exit.

You’re welcome!

From your description, use of Operations sounds reasonable, especially if you are calling in to async code. Whilst you could synchronise that stuff by blocking the main queue and waiting, the danger is that they might also try to execute some callbacks on the main queue, resulting in a deadlock.

That’s the other special property of the main queue - everyone (as in, any piece of code in any library) can easily access it, so it sometimes gets used as a "default" place to execute callbacks in code which doesn’t bother to ask for an explicit callback queue.

Keeping everything async and not blocking the main queue means you don’t have to worry about that. Overall, it probably makes your app simpler to continue doing things as you are.

1 Like

Thanks a lot @Karl I learned a lot !

You can also use the exit(withError:) static method on your command type to exit a little more fluently than C exit(_:).

1 Like

Thanks @zwaldowski that’s pretty cool. Didn’t know that.

Terms of Service

Privacy Policy

Cookie Policy