The Problem with Shared State
Discover the problem with the shared state and learn its solution.
We'll cover the following...
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?
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 bydownloaded
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, anddownloaded
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 }
Because multiple threads interact with the same instance, the code above will print a number smaller than ...