動機
ある値群へのアクセスを排他制御下で行う必要がある場合、
それらの値へのアクセスは基本的に全て排他制御下で行う必要がある。
しかしそれらのアクセスを行う箇所が多数ある場合に排他制御が抜けてしまったり、
他の人がメンテナンスする際に排他制御が必要なことに気付かなかったりする恐れがある。
そこで、排他制御を強制する仕組みを検討した。
2つの値へのアクセスを排他制御されたブロック内で行うことを強制するクラス
今回考えた仕組みでは、少なくとも排他制御を強制したい値の数ごとにクラスが必要になる。
ここでは値が2つの場合のものを紹介する。
import java.io.Closeable
/**
* 2つの値へのアクセスを排他制御されたブロック内で行うことを強制するクラス。
*
* 次のようにして使う。
*
* ```
* val values = SynchronizedValues2<Int, Int>(1, 1)
*
* val result =
* values.synchronized {
* // このブロック内は排他制御されている。
* // このブロック内では、コンストラクタで初期値を設定した2つの値 value0, value1 にアクセスできる。
*
* (value0 + value1).also {
* value0 = value1
* value1 = it
* }
* }
* ```
*
* @param T0 排他制御されたブロック内でのみアクセスを許したい値0の型
* @param T1 排他制御されたブロック内でのみアクセスを許したい値1の型
* @param value0 排他制御されたブロック内でのみアクセスを許したい値0の初期値
* @param value1 排他制御されたブロック内でのみアクセスを許したい値1の初期値
*/
class SynchronizedValues2<T0, T1>(
private var value0: T0,
private var value1: T1
) {
private val lock = Any()
/**
* 排他制御されたブロック内で値にアクセスする。
*
* @param block 排他制御された環境下で実行される処理。
* @return [block] を実行した結果の値。
*/
fun <R> synchronized(block: Context<T0, T1>.() -> R): R {
return synchronized(lock) {
Context(this).use {
it.block()
}
}
}
class Context<T0, T1>(
values: SynchronizedValues2<T0, T1>
) : Closeable {
private var _values: SynchronizedValues2<T0, T1>? = values
private val values: SynchronizedValues2<T0, T1>
get() = checkNotNull(_values) { "このコンテキストは既に破棄されています。" }
var value0: T0
get() = values.value0
set(value) {
values.value0 = value
}
var value1: T1
get() = values.value1
set(value) {
values.value1 = value
}
override fun close() {
_values = null
}
}
}
使用方法
クラスのコメントに書いたとおりだが、次のように使用する。
val values = SynchronizedValues2<Int, Int>(1, 1)
val result =
values.synchronized {
// このブロック内は排他制御されている。
// このブロック内では、コンストラクタで初期値を設定した2つの値 value0, value1 にアクセスできる。
(value0 + value1).also {
value0 = value1
value1 = it
}
}
改善すべき点
- 値の名前が固定(
value0
,value1
)である。
任意の名前を付けられるようにするには、コードの自動生成などを行う必要がある。
動作確認
1から1000までの値に対して、数と合計を求めた。
このクラスを使わなかった場合
fun main() {
runBlocking {
val stats = object {
var count = 0
var total = 0L
}
launch {
(1..1000).forEach {
launch(Dispatchers.Default) {
delay(1) // 競合が発生しやすくする。
++stats.count
stats.total += it
delay(1) // 競合が発生しやすくする。
}
}
}.join()
println("count: ${stats.count}, total: ${stats.total}")
}
}
結果例: count: 994, total: 498509
このクラスを使った場合
fun main() {
runBlocking {
val stats = SynchronizedValues2<Int, Long>(0, 0L)
launch {
(1..1000).forEach {
launch {
delay(1)
stats.synchronized {
++value0
value1 += it
}
delay(1)
}
}
}.join()
stats.synchronized {
println("count: $value0, total: $value1")
}
}
}
結果例: count: 1000, total: 500500
/以上