Skip to content

What is the difference between setValue and update for MutableStateFlow? #3529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
turkergoksu opened this issue Nov 16, 2022 · 2 comments
Closed

Comments

@turkergoksu
Copy link

turkergoksu commented Nov 16, 2022

I was reading Michael Ferguson's Atomic Updates on MutableStateFlow post which was saying that there is a difference between setValue and update. Then I checked documentation for value and it was saying;

This property is thread-safe and can be safely updated from concurrent coroutines without external synchronization.

And I thought If we don't need any external synchronization then we can use both value and update for atomic operations. I've tried the post's example like below which was behaving same.

Example:

data class Dummy(
    val title: String = "Initial Title",
    val count: Int = 0,
)

fun main() {
    val state = MutableStateFlow(Dummy())

    runBlocking {
        launch(Dispatchers.IO) {
            state.value = state.value.copy(title = "New Title") // Comment out to test update function
//            state.update { it.copy(title = "New Title") } // Uncomment to test update function
            println(state.value)
        }
        launch(Dispatchers.Default) {
            state.value = state.value.copy(count = 1) // Comment out to test update function
//            state.update { it.copy(count = 1) } // Uncomment to test update function
            println(state.value)
        }
    }
}

// Output of setValue:
// Dummy(title=New Title, count=0)
// ...
// Dummy(title=New Title, count=1)

// Output of update:
// Dummy(title=New Title, count=0)
// ...
// Dummy(title=New Title, count=1)

Am I interpreting documentation wrong? Isn't it not needing any external synchronization = it's an atomic operation 🤔

@psteiger
Copy link

psteiger commented Nov 16, 2022

Reading the value is atomic, setting the value is atomic, but reading and setting is not (two atomic operations don't make one atomic operation without any kind of external synchronization).

The following race is possible:

  1. state.value starts with 0
  2. Two threads do state.value = state.value + 1 concurrently.
  3. In thread A, right side of operation in (1) evaluates state.value, which is 0, and sums with 1.
  4. Before the assignment by thread A happens (and after state.value was already evaluated), thread B reads state.value, which is 0, adds 1, and updates state.value to 1.
  5. Thread A finally assigns state.value to 1 (left side of operation in (1)
  6. You probably expected state.value to be "2" in this example, but it will be 1 ultimately.

That's where StateFlow.update comes in.

@turkergoksu
Copy link
Author

turkergoksu commented Nov 17, 2022

Thanks for taking time to answer. Separating read and write operations make me understood their difference now. update function enables us to read and write atomically within that lambda function with it value. However if we use setValue we also need to read value with state.value which might've changed in some coroutine.

I've changed the example to launch more than two coroutines;

fun main() {
    val loopCount = 100
    val state = MutableStateFlow(0)

    val numList = mutableListOf<Int>()
    val diffList = mutableListOf<Int>()

    runBlocking {
        repeat(loopCount) {
            launch(Dispatchers.Default) {
                // Comment to test update function
                val new = state.value + 1
                state.value = new
                println(new)

                // Uncomment to test update function
//                state.update {
//                    numList.add(it + 1)
//                    it + 1
//                }
            }
        }
    }

    runBlocking {
        for (num in 1..loopCount) {
            if (numList.contains(num).not()) {
                diffList.add(num)
            }
        }
        println("numList: $numList")
        println("numList size: " + numList.size)
        println("missing number list: $diffList")
        println("last value of state: " + state.value)
    }
}

Output example for setValue:

numList: [1, 2, 3, 3, 4, 5, ..., 95, 96, 97]
numList size: 100 // We may think of this how many times numList.add function called. For setValue it's fixed to loopCount
missing number list: [98, 99, 100] // Some values missed
last value of state: 97 // Last value is not equal to loopCount which we don't want

Output example for update

numList: [1, 2, 3, 3, 4, 5, ..., 98, 99, 100]
numList size: 108 // Since update works with while(true) sometimes its called more than loopCount.
missing number list: [] // No missing value
last value of state: 100 // Last value is equal to loopCount which what we want

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants