swiftconcurrency

Async let - fire-and-forget


I want to fire off multiple calls in parallel but I do not care for their results. I'd like to use async let syntax for it.

func callA() async {
    print("call a")
}

func callB() async {
    print("call b")
}

func callC() async {
    print("call c")
}


func makeCalls() async {
    async let a: Void = callA()
    async let b: Void = callB()
    async let c: Void = callC()
}


func someFuncThatReturnsImmediately() {
    Task {
        await makeCalls()
    }
}

This seems to work for this example, but can I be sure that all calls will be made and the task is not cancelled if I don't use await?

Also I get warnings for the unused vars

Immutable value 'a' was never used; consider replacing with '_' or removing it

but if I do replace it that results in an error

Expression is 'async' but is not marked with 'await'

Is there a way to do this without a warning?


Solution

  • async let a: Void = callA() should work, but you may make some adjustments.

    Fix the warning:

    "Immutable value 'a' was never used; consider replacing with '_' or removing it"

    Fix:

    func makeCalls() async {
        async let _ = callA()
        async let _ = callB()
        async let _ = callC()
    }
    
    

    UPDATE

    As mentioned by Robs answer, the function makeCalls() as is does not work yet. The sole definition of the 'async let' constant is not sufficient: you also need to await the task, otherwise it will be cancelled before the function returns. Since the task does not return a result, this can be quickly overlooked. Thanks Rob!

    func makeCalls() async {
        async let a: Void = callA()
        async let b: Void = callB()
        async let c: Void = callC()
        _ = await a
        _ = await b
        _ = await c
    
    }
    

    Note also, that in your code as is, there is no difference with the code above, since task cancellation is cooperative and your tasks do nothing on a cancellation request. So, despite your callA, callB and callC task will be cancelled in the first version of makeCall(), they do ignore this cancellation request and run until completion.

    can I be sure that all calls will be made and the task is not cancelled?

    If I understand the question correctly, and if we only examine the function below and do not make any other assumptions:

    await makeCalls()
    

    IFF there is a task where makeCalls() has been called, and this task will be cancelled, all your child tasks will be cancelled as well. Note, that in structured currency, every task has a parent task, and there will be eventually a root task. In structured concurrency, if a task gets cancelled all child tasks get cancelled as well, and hence – iff structured concurrency – all their child tasks as well.

    However, if you create your own task explicitly (NOT structured concurrency), like here:

    func someFuncThatReturnsImmediately() {
        Task {
            await makeCalls()
        }
    }
    

    then, since you have no reference to the task and no code that does cancel the task explicitly, the task will run until completion, i.e. when all child tasks have been finished.

    Note though, that your tasks neither return a result nor do they throw an error. The caller has no way to get noticed about cancellation. The only way to perceive somewhere something is by observing the side effects of the tasks themselves – for example, they may print out a log statement.

    Alternative

    If you want to run tasks in parallel and as "fire & forget", you may also run them within a TaskGroup:

    func fireAndForgetSomeTasks() {
        Task {
            await withDiscardingTaskGroup { taskGroup in
                taskGroup.addTask { await callA() }
                taskGroup.addTask { await callB() }
                taskGroup.addTask { await callC() }
            }
        }
    }
    

    Note that there are various ways to use a TaskGroup which is very powerful. Please also read the documentation.

    Note also, that Task cancellation is cooperative. That means, that any task which will be cancelled (better: a request to cancel the task is made) needs to take either proactive measurements or needs to poll the cancellation state in order to handle the cancellation request.