こんにちは。
bitFlyerのAdvent Calendar 4日目の記事です。
当日に締め切りに気付いて大慌てでKMMについて書こうとしたのですが、iOSの環境構築(M1へのpodの導入、実機デバイスへのインストールなど)でどハマりしてしまい・・・
なんとか解決したのですが気付いた頃には現在21時、題材変更を決めました。
今回は MutableStateFlowのupdate()とvalueの違いについて 書きたいと思います。
皆さんはStateFlowを使ったことはありますでしょうか。
実は私はずっとLiveDataを使っていて、初めて使ったのは一年半前だったりします。
ところで、LiveDataの値を書き換えるときは
_liveData.value = "updated value"
こんな風に書きますよね。
でもメインスレッド以外でこれをやると落ちるので
_liveData.postValue("updated value")
こんな風に書いたりした記憶があります。
でもStateFlowなら簡単!
val currentValue = _stateFlow.value
_stateFlow.value = "$currentValue updated"
これでオッケーです。
「StateFlowは値を書き換えるときに余計なことを考えなくて良くて安全だな。.value
を使うだけ!」と何も考えずに使っていたのですが、なんと最近、新事実を知りました。
_stateFlow.update {
val currentValue = _stateFlow.value
"$currentValue updated"
}
上記の書き方でも値を書き換えられるらしいです。
「え、.value
で書き換えてもLiveDataみたいな事故は起きたことなかったけど、このメソッド要る???普通に書き換えるのと何が違うの??」と疑問に思い調べてみました。
目次
1.updateの実装を読んでみる
2.シミュレーションしてみる
3.おわりに
1. updateの実装を読んでみる
早速実装を読んでみます。
/**
* Updates the [MutableStateFlow.value] atomically using the specified [function] of its value.
*
* [function] may be evaluated multiple times, if [value] is being concurrently updated.
*/
public inline fun <T> MutableStateFlow<T>.update(function: (T) -> T) {
while (true) {
val prevValue = value
val nextValue = function(prevValue)
if (compareAndSet(prevValue, nextValue)) {
return
}
}
}
ドキュメントコメントを読む限りでは、「引数のfunctionを用いてアトミックにvalueを更新する。同時にvalueが更新されたときに複数回funcitonが評価され得る」というような内容が書かれています。
「スレッドセーフってことなのかな。でも複数回functionが評価されるのはなぜ?」などなど色々疑問が残りました。
次に、コードを読んでみましょう。
whileがついていて、compareAndSet()
がtrueになるまで繰り返すようです。(compareAndSet()
については後述します)
val prevValue = value
については、現時点におけるStateFlowのvalueを保持しているだけなので特筆すべき点はないかと思います。
val nextValue = function(prevValue)
についてはなかなか理解するのが難しかったです。この行が何をしているのか詳しくみていきたいと思います。
まず、このfunction()はなにかというと、update()の引数に渡したクロージャです。
update()の使い方を思い出してみると
_stateFlow.update {
val currentValue = _stateFlow.value
"$currentValue updated"
}
引数が一つなので括弧を省略していますが、クロージャを渡しています。
続いて、function(prevValue)
において、なぜprevValue
をいれたのか、についてですが、クロージャ内で使うためのようです。
さっきのupdate()の使い方を少し修正して
_stateFlow.update { currentValue ->
"$currentValue updated"
}
↑こんな風に書けるので、ちょっと綺麗になりました。
また、nextValueはfunction()の返り値を受けているので、update {} の波括弧内の返り値がnextValueに入る ことがわかります。
つまり、nextValueには"$currentValue updated"
が入っています。
nextValueの正体がわかったところで、compareAndSet()
についてです。
これは、prevValueとStateFlow.value
が異なっていた場合はfalseを返してくれるようです。(中身を見てみましたが複雑すぎて厳密にはわかりませんでした。興味のある方は覗いてみてください)
つまり、結論としては prevValueという変数にupdate()が走り始めた直後のStateFlowのvalueを保持して、nextValueにおいてクロージャを実行し、その実行時間を待ち、その待ち時間中にStateFlowのvalueが変わってしまっていたらwhileがもう一周する、という仕組みです。
「update中にvalueが変わってしまっていた」という現象は、マルチスレッドで重い処理をすると十分に起こり得ます。
実際に、マルチスレッドで時間がかかる処理を行ってvalueを書き換えたときに何が起こるのかシミュレーションしてみました。
2. シミュレーションしてみる
MutableStateFlow.value = ~
を使った場合
皆さんも下記のコードのanswerを予想してみてください。
マルチスレッドで3回ずつ値を書き換えています。
@Test
fun question1() {
val stateFlow = MutableStateFlow("current value")
runBlocking {
listOf(
async {
for (i in 0 until 3) {
val text = stateFlow.value
// あれこれtextを加工する処理に100msかかると仮定
delay(100)
stateFlow.value = "$text updated"
}
},
async {
for (i in 0 until 3) {
val text = stateFlow.value
// あれこれtextを加工する処理に110msかかると仮定
delay(110)
stateFlow.value = "$text updated2"
}
},
).awaitAll()
}
val answer = stateFlow.value
}
Q1. answerの変数に入っている文字列はなんでしょうか。
_
_
_
_
_
_
_
_
_
_
A. current value updated2 updated2 updated2
でした!
updated
を足した直後に、updated2
で上書きされてしまっているからですね。(二つ目のスレッドのtextに保持された値にはupdated
が入っていないため)
いざこうして書かれると当たり前に感じますが、盲点な気がします。
「本番で稀に起きるけどどうしても再現できないバグ」は意外とこういうところに潜んでいるのかもしれません。
MutableStateFlow.update()
を使った場合
続いて第二問です!
@Test
fun question() {
val stateFlow = MutableStateFlow("current value")
runBlocking {
listOf(
async(Dispatchers.IO) {
for (i in 0 until 3) {
stateFlow.update { text ->
// あれこれtextを加工する処理に100msかかると仮定
delay(100)
"$text updated"
}
}
},
async(Dispatchers.IO) {
for (i in 0 until 3) {
stateFlow.update { text ->
// あれこれtextを加工する処理に110msかかると仮定
delay(110)
"$text updated2"
}
}
},
).awaitAll()
}
val answer = stateFlow.value
}
Q2. answerの変数に入っている文字列はなんでしょうか。
_
_
_
_
_
_
_
_
_
_
A. current value updated updated updated updated2 updated2 updated2
- で起きた事故が未然に防がれていますね!
きちんと全ての変更が反映されています!
期待通りでした。
3. おわりに
StateFlowのvalueを、マルチスレッドかつ時間がかかる処理をして書き換える場合はupdate()を使いましょう。
軽めの記事にするつもりだったのですがだいぶ重たい記事になってしまいました。
ここまでお付き合いいただきありがとうございました!
また、弊社bitFlyerではAndroidエンジニアを募集しています。ご興味のある方はぜひご応募ください!