I have the following code which has a race condition. I try to find an item in a list and set its loading property. But if onLoaded("A") and onLoaded("B") are called multiple times from different threads. I always lose the data of the first call if it doesn't complete before second starts.
How can I make this work? Is using Mutex should be the correct approach?
val list = MutableLiveData<List<Model>>() // assume this is initialized with ["Model(false, "A"), Model(false, "B")]
data class Model(
val loaded: Boolean,
val item: String,
)
fun onLoaded(item: String) = viewModelScope.launch {
val currList = list.value ?: return#launch
withContext(Dispatchers.Default) {
val updated = currList.find { it.item == item }?.copy(loaded = true)
val mutable = currList.toMutableList()
updated?.let {
val index = mutable.indexOf(it)
mutable[index] = it
}
list.postValue(mutable.toList())
}
}
onLoaded("A")
onLoaded("B")
expected: ["Model(true, "A"), Model(true, "B")]
actual: ["Model(false, "A"), Model(true, "B")]
In onLoaded() a new coroutine is launched using viewModelScope. viewModelScope has Dispatchers.Main.immediate context, so the code inside it will be executed on the Main Thread, e.g. execution is limited to only one thread. The reason you have a Race Condition because calling the onLoaded() function consecutively doesn't guarantee the order of coroutines execution.
If you call onLoaded() consecutively from one thread I suggest to remove launching a coroutine viewModelScope.launch in it. Then the order of calling will be preserved. Use list.postValue() in this case.
If you call onLoaded() from different threads and still want to launch a coroutine you can refer to answers to this question.
Try to use #Synchronized anotation without launching a coroutine:
#Synchronized
fun onLoaded(item: String) { ... }
Method will be protected from concurrent execution by multiple threads by the monitor of the instance on which the method is defined. Use list.postValue() in this case.
Related
I use livedata that collect item data, after it in onClicked() I get this data from livedata. What could be better approach for this?
lifecycleScope.launch {
lifecycleScope.async {
viewModel.fetchItem(args.workId)
}.await()
onClicked()
}
variables in viewModel
val item = _item.immutable()
private val _item = MutableLiveData<ViewState<Item?>>()
[...]
// I wait here for data from first code than I collect it because item is liveData as above
private fun onClicked() {
val item = viewModel.item.value?.dataOrNull
[...]
fetchItem method
fun fetchItem(id: Int) {
viewModelScope.launch {
_item.postAsyncValue { repository.getItem(id) }
}
}
Currently, I think your code is not doing what you think it does. fetchItem is not a suspend function. It launches a coroutine and immediately returns without waiting for the coroutine to finish. So, your async coroutine that calls it doesn't wait for that result either. There's no point in using async at all here since the code returns almost immediately.
I'm guessing that what you're trying to accomplish is wait for postAsyncValue to finish setting that new value. To do this, you need to make fetchItem a suspend function that waits for its work to be done.
I am not familiar with this postAsyncValue, but my best guess is that it is a helper extension function on MutableLiveData that takes a suspend lambda, calls it, and then sets the value to the LiveData on the main thread. If this is the case, you should just do this instead so the function actually waits for the task to be done before returning:
suspend fun fetchItem(id: Int) = withContext(Dispatchers.Main) {
_item.value = repository.getItem(id) // assuming getItem is a suspend function
}
//...
lifecycleScope.launch {
viewModel.fetchItem(args.workId)
onClicked()
}
If this isn't right, please add your source code for postAsyncValue and let me know.
Regarding your literal question, instead of using async followed immediately by await, you can use withContext(Dispatchers.IO) { }, but you would only do this if you are calling blocking code. You only need async when you're working with parallelism, so the compiler warns you that you're doing something silly if you immediately call await() after async { }.
I have an instance of CoroutineScope and log() function which look like the following:
private val scope = CoroutineScope(Dispatchers.IO)
fun log(message: String) = scope.launch { // launching a coroutine
println("$message")
TimeUnit.MILLISECONDS.sleep(100) // some blocking operation
}
And I use this test code to launch coroutines:
repeat(5) { item ->
log("Log $item")
}
The log() function can be called from any place, in any Thread, but not from a coroutine.
After a couple of tests I can see not sequential result like the following:
Log 0
Log 2
Log 4
Log 1
Log 3
There can be different order of printed logs. If I understand correctly the execution of coroutines doesn't guarantee to be sequential. What it means is that a coroutine for item 2 can be launched before the coroutine for item 0.
I want that coroutines were launched sequentially for each item and "some blocking operation" would execute sequentially, to always achieve next logs:
Log 0
Log 1
Log 2
Log 3
Log 4
Is there a way to make launching coroutines sequential? Or maybe there are other ways to achieve what I want?
Thanks in advance for any help!
One possible strategy is to use a Channel to join the launched jobs in order. You need to launch the jobs lazily so they don't start until join is called on them. trySend always succeeds when the Channel has unlimited capacity. You need to use trySend so it can be called from outside a coroutine.
private val lazyJobChannel = Channel<Job>(capacity = Channel.UNLIMITED).apply {
scope.launch {
consumeEach { it.join() }
}
}
fun log(message: String) {
lazyJobChannel.trySend(
scope.launch(start = CoroutineStart.LAZY) {
println("$message")
TimeUnit.MILLISECONDS.sleep(100) // some blocking operation
}
)
}
Since Flows are sequential we can use MutableSharedFlow to collect and handle data sequentially:
class Info {
// make sure replay(in case some jobs were emitted before sharedFlow is being collected and could be lost)
// and extraBufferCapacity are large enough to handle all the jobs.
// In case some jobs are lost try to increase either of the values.
private val sharedFlow = MutableSharedFlow<String>(replay = 10, extraBufferCapacity = 10)
private val scope = CoroutineScope(Dispatchers.IO)
init {
sharedFlow.onEach { message ->
println("$message")
TimeUnit.MILLISECONDS.sleep(100) // some blocking or suspend operation
}.launchIn(scope)
}
fun log(message: String) {
sharedFlow.tryEmit(message)
}
}
fun test() {
val info = Info()
repeat(10) { item ->
info.log("Log $item")
}
}
It always prints the logs in the correct order:
Log 0
Log 1
Log 2
...
Log 9
It works for all cases, but need to be sure there are enough elements set to replay and extraBufferCapacity parameters of MutableSharedFlow to handle all items.
Another approach is
Using Dispatchers.IO.limitedParallelism(1) as a context for the CoroutineScope. It makes coroutines run sequentially if they don't contain calls to suspend functions and launched from the same Thread, e.g. Main Thread. So this solution works only with blocking (not suspend) operation inside launch coroutine builder:
private val scope = CoroutineScope(Dispatchers.IO.limitedParallelism(1))
fun log(message: String) = scope.launch { // launching a coroutine from the same Thread, e.g. Main Thread
println("$message")
TimeUnit.MILLISECONDS.sleep(100) // only blocking operation, not `suspend` operation
}
It turns out that the single thread dispatcher is a FIFO executor. So limiting the CoroutineScope execution to one thread solves the problem.
I have an instance of CoroutineScope and log() function which look like the following:
private val scope = CoroutineScope(Dispatchers.IO)
fun log(message: String) = scope.launch { // launching a coroutine
println("$message")
TimeUnit.MILLISECONDS.sleep(100) // some blocking operation
}
And I use this test code to launch coroutines:
repeat(5) { item ->
log("Log $item")
}
The log() function can be called from any place, in any Thread, but not from a coroutine.
After a couple of tests I can see not sequential result like the following:
Log 0
Log 2
Log 4
Log 1
Log 3
There can be different order of printed logs. If I understand correctly the execution of coroutines doesn't guarantee to be sequential. What it means is that a coroutine for item 2 can be launched before the coroutine for item 0.
I want that coroutines were launched sequentially for each item and "some blocking operation" would execute sequentially, to always achieve next logs:
Log 0
Log 1
Log 2
Log 3
Log 4
Is there a way to make launching coroutines sequential? Or maybe there are other ways to achieve what I want?
Thanks in advance for any help!
One possible strategy is to use a Channel to join the launched jobs in order. You need to launch the jobs lazily so they don't start until join is called on them. trySend always succeeds when the Channel has unlimited capacity. You need to use trySend so it can be called from outside a coroutine.
private val lazyJobChannel = Channel<Job>(capacity = Channel.UNLIMITED).apply {
scope.launch {
consumeEach { it.join() }
}
}
fun log(message: String) {
lazyJobChannel.trySend(
scope.launch(start = CoroutineStart.LAZY) {
println("$message")
TimeUnit.MILLISECONDS.sleep(100) // some blocking operation
}
)
}
Since Flows are sequential we can use MutableSharedFlow to collect and handle data sequentially:
class Info {
// make sure replay(in case some jobs were emitted before sharedFlow is being collected and could be lost)
// and extraBufferCapacity are large enough to handle all the jobs.
// In case some jobs are lost try to increase either of the values.
private val sharedFlow = MutableSharedFlow<String>(replay = 10, extraBufferCapacity = 10)
private val scope = CoroutineScope(Dispatchers.IO)
init {
sharedFlow.onEach { message ->
println("$message")
TimeUnit.MILLISECONDS.sleep(100) // some blocking or suspend operation
}.launchIn(scope)
}
fun log(message: String) {
sharedFlow.tryEmit(message)
}
}
fun test() {
val info = Info()
repeat(10) { item ->
info.log("Log $item")
}
}
It always prints the logs in the correct order:
Log 0
Log 1
Log 2
...
Log 9
It works for all cases, but need to be sure there are enough elements set to replay and extraBufferCapacity parameters of MutableSharedFlow to handle all items.
Another approach is
Using Dispatchers.IO.limitedParallelism(1) as a context for the CoroutineScope. It makes coroutines run sequentially if they don't contain calls to suspend functions and launched from the same Thread, e.g. Main Thread. So this solution works only with blocking (not suspend) operation inside launch coroutine builder:
private val scope = CoroutineScope(Dispatchers.IO.limitedParallelism(1))
fun log(message: String) = scope.launch { // launching a coroutine from the same Thread, e.g. Main Thread
println("$message")
TimeUnit.MILLISECONDS.sleep(100) // only blocking operation, not `suspend` operation
}
It turns out that the single thread dispatcher is a FIFO executor. So limiting the CoroutineScope execution to one thread solves the problem.
I have switched my context to Dispatcher.Main to show UI but data fetched on runBlocking but unable to display in RecylerView
runBlocking {
var fruits = fetchFruitList()
withContext(Dispatchers.Main){
recyclerView.adapter = FruitAdapter(fruits);
}
}
what am I doing wrong and what is the appropriate way to return data from one Dispatcher to another.
I have tried another way
GlobalScope.launch {
withContext(Dispatchers.IO){
var fruits = arrayOf("Grapes","Apple","Mango","TuttiFruit","PineApple",
"Pomegrante","Apple","Mango","TuttiFruit","PineApple",
"Pomegrante","Apple","Mango","TuttiFruit","PineApple").toList()
return#withContext
}
recyclerView.adapter = FruitAdapter(fruits)
}
but in above way I have to declare fruits as global whereas I don't want to have it global to work. Is there a way to return data from one 'dispatcher queue to another
I have to fetch data from Api (IO operation) and display that data in RecyclerView(Main Thread Operation)
That's because you must switch the context after the data is fetched:
GlobalScope.launch(Dispatchers.IO){
var fruits = arrayOf("Grapes","Apple","Mango","TuttiFruit","PineApple",
"Pomegrante","Apple","Mango","TuttiFruit","PineApple",
"Pomegrante","Apple","Mango","TuttiFruit","PineApple").toList()
withContext(Dispatchers.MAIN){
recyclerView.adapter = FruitAdapter(fruits)
}
}
Edit According to the comments:
For the runBlocking check out the documentation first paragraph.
Runs a new coroutine and blocks the current thread interruptibly until
its completion. This function should not be used from a coroutine. It
is designed to bridge regular blocking code to libraries that are
written in suspending style, to be used in main functions and in
tests.
Secondly, you ask for the GlobalScope usage. Yea, if you are doing coroutines in Android you should avoid that. Reasons here.
How to launch a coroutine in Android Activity/Fragment?
First I suggest to use in the ViewModel or the Presenter but if you want to launch a coroutine in the Activity/Fragment, you will need a way to control and manage it's cancellation to avoid memory leak.
The solution for this would be:
private val job: Job = Job()
//or Dispatchers.IO
private val fragmentScope = CoroutineScope(Dispatchers.MAIN + job)
//launch a coroutine later in Activity/Fragment
fragmentScope.launch{
//the default coroutine dispatcher would be the defined dispatcher above
}
override fun onDestroy(){
super.onDestroy()
fragmentScope.cancel()
}
As for your question:
what am I doing wrong and what is the appropriate way to return data
from one Dispatcher to another
You can also try this solution if you want to return values from a different context:
someScope.launch(Dispatchers.MAIN){
var data = withContext(Dispatchers.IO){
val someData = fetchSomeData()
return#withContext data
}
if(data.isAvailable()){ //for example
//runing on the main thread
}
}
I am using a callback function when async ends. but it doesn't work well :(
my case:
fun function1(callback : (obj1: List<ObjT1>,obj2: List<ObjT1>)-> Unit?){
doAsync {
//long task
uiThread { callback(result1, result2) }
}
}
the callback is called but result1 and result2(lists) are empty. I checked previous the content of the list.
EDIT:
PROBLEM: my callback is a function that receives 2 objects result 1 and result2, the problem is the function callback sometimes receives the results empty, i check their content and is not empty.
It may be because you've declared return type as Unit? but are returning two values. A quick fix would be to put result1 and result2 in an array.
Now this question is about deprecated Kotlin library.
I recommend use coroutines.
Consider using Kotlin's coroutines. Coroutines is a newer feature in Kotlin. It is still technically in it's experimental phase, but JetBrains has told us that it is very stable.
Read more here: https://kotlinlang.org/docs/reference/coroutines.html
Here is some sample code:
fun main(args: Array<String>) = runBlocking { // runBlocking is only needed here because I am calling join below
val job = launch(UI) { // The launch function allows you to execute suspended functions
val async1 = doSomethingAsync(250)
val async2 = doSomethingAsync(50)
println(async1.await() + async2.await()) // The code within launch will
// be paused here until both async1 and async2 have finished
}
job.join() // Just wait for the coroutines to finish before stopping the program
}
// Note: this is a suspended function (which means it can be "paused")
suspend fun doSomethingAsync(param1: Long) = async {
delay(param1) // pause this "thread" (not really a thread)
println(param1)
return#async param1 * 2 // return twice the input... just for fun
}