LoginSignup
1
4

More than 3 years have passed since last update.

【Kotlin】排他制御を強制する

Last updated at Posted at 2019-09-21

動機

ある値群へのアクセスを排他制御下で行う必要がある場合、
それらの値へのアクセスは基本的に全て排他制御下で行う必要がある。
しかしそれらのアクセスを行う箇所が多数ある場合に排他制御が抜けてしまったり、
他の人がメンテナンスする際に排他制御が必要なことに気付かなかったりする恐れがある。

そこで、排他制御を強制する仕組みを検討した。

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

/以上

1
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
4