...

/

The Problem with Shared State

The Problem with Shared State

Discover the problem with the shared state and learn its solution.

Before we start, take a look at the UserDownloader class below. It allows us to fetch a user by ID or get all the users downloaded before. What’s wrong with this implementation?

Press + to interact
class UserDownloader(
private val api: NetworkService
) {
private val users = mutableListOf<User>()
fun downloaded(): List<User> = users.toList()
suspend fun fetchUser(id: Int) {
val newUser = api.fetchUser(id)
users.add(newUser)
}
}

Note: Notice the use of the defensive copy toList. We did this to avoid a conflict between reading the object returned by downloaded and adding an element to the mutable list. We could also represent users using the read-only list (List<User>) and the read-write property (var). Then, we would not need to make a defensive copy, and downloaded would not need to be protected at all, but we would decrease the performance of adding elements to the collection. We prefer the second approach, but we’ve decided to show the one using a mutable collection as we see it more often in real-life projects.

The implementation above is not prepared for concurrent use. Each fetchUser call modifies users. This is fine if this function is not simultaneously started on more than one thread. Since we can start it on more than one thread simultaneously, we say users is a shared state; therefore, it needs to be secured. This is because concurrent modifications can lead to conflicts. This problem is presented below.

package kotlinx.coroutines.app 
import kotlinx.coroutines.*

class UserDownloader(
    private val api: NetworkService
) {
    private val users = mutableListOf<User>()

    fun downloaded(): List<User> = users.toList()

    suspend fun fetchUser(id: Int) {
        val newUser = api.fetchUser(id)
        users += newUser
    }
}

class User(val name: String)

interface NetworkService {
    suspend fun fetchUser(id: Int): User
}

class FakeNetworkService : NetworkService {
    override suspend fun fetchUser(id: Int): User {
        delay(2)
        return User("User$id")
    }
}

suspend fun main() {
    val downloader = UserDownloader(FakeNetworkService())
    coroutineScope {
        repeat(1_000_000) {
            launch {
                downloader.fetchUser(it)
            }
        }
    }
    print(downloader.downloaded().size) // ~998242
}
Problem of concurrent use

Because multiple threads interact with the same instance, the code above will print a number smaller than 1,000, ...