Home > OS >  Appropriate use of coroutines for parallel API calls
Appropriate use of coroutines for parallel API calls

Time:10-12

I'm trying to learn the right way to use Kotlin coroutines to do some parallel API calls. I've been able to get the expected results/process, but am wondering if there is a "more correct" way to do so.

Here's my contrived example of what I'm trying to do. Use case is essentially an inbound web request, and that request in turn needing to make a couple of API calls. Digging around led me to using launch or async:

// using launch
var recordExists = false
var otherRecordExists = false

coroutineScope {
    launch {
        recordExists = someHttpService.doesRecordExist(123)
    }
    launch {
        otherRecordExists = someHttpService.doesRecordExist(456)
    }
}

if (!recordExists || !otherRecordExists) {...}

vs

// using async
var recordExists = false
var otherRecordExists = false

coroutineScope {
    val recordDeferred = async { someHttpService.doesRecordExist(123) }
    val otherRecordDeferred = async { someHttpService.doesRecordExist(456) }

    recordExists = recordDeferred.await()
    otherRecordExists = otherRecordDeferred.await()
}

if (!recordExists || !otherRecordExists) {...}

I landed on using launch because it felt cleaner, but not sure if that was the most appropriate decision reasoning...

Is there a specific reason to use one over the other that I'm overlooking?

CodePudding user response:

Your code is okay but as a general practice, I would recommend avoiding using var as much as possible when dealing with concurrency and parallelism. Having a shared mutable state is very risky and can often lead to race-conditions. In your case you can do something like:

coroutineScope {
    val recordDeferred = async { someHttpService.doesRecordExist(123) }
    val otherRecordDeferred = async { someHttpService.doesRecordExist(456) }

    val recordExists = recordDeferred.await()
    val otherRecordExists = otherRecordDeferred.await()

    if (!recordExists || !otherRecordExists) {
        ...
    }
}

Here you can only use async as you need to get some data back from the coroutine.

CodePudding user response:

Your First approach will

  • Execute 2 parallel calls, and afaik, theres no guarantee who will start or finish first
  • Will return a job where you can perform specific operations on your launched coroutines

Your second approach will

  • Same with first's first bullet
  • Will return a deferred object where you can read a data the async returns via await from a type you specified.

Based on experience, launch will suffice most of the things we need for suspended/concurrent operations with coroutines, but keep in mind AFAIK when performing operations using async you have to make sure that there will be an await call somewhere, otherwise if some exception happened inside an asnyc operation without a corresponding await call, that exception will be swallowed and you will be left debugging blindly, which is different with launch, any exception thrown inside of a launch operation will propagate to all nesting coroutines up to the top, though such use-cases of CancellationException propagation can be handled by structuring your Couroutines with a Supervisor Job

Take my answer with a grain of salt and I'm open for any correction.

Also please have a read with some related posts and topics

What is the difference between launch/join and async/await in Kotlin coroutines

Coroutines

Supervisor Job

  • Related