Created
April 6, 2023 08:25
-
-
Save mikehearn/1913202829403f65331123f0473a7901 to your computer and use it in GitHub Desktop.
A class that makes it easy to wrap state such that it's only accessible when locked.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package hydraulic.kotlin.utils | |
import kotlin.contracts.ExperimentalContracts | |
import kotlin.contracts.InvocationKind | |
import kotlin.contracts.contract | |
/** | |
* A wrapper class that makes it harder to forget to take a lock before accessing some shared state. | |
* | |
* Simply define an anonymous object to hold the data that must be grouped under the same lock, and then pass it | |
* to the constructor. You can now use the [locked] method with a lambda to take the object lock in a | |
* way that ensures it'll be released if there's an exception. Kotlin's scoping rules will ensure you can only | |
* access the fields by using either [locked] or `__unlocked`, thus making it clear at each use-site which it is. | |
* You should generally not use `__unlocked`, it is public only because [locked] is an inlined function. | |
* | |
* This technique is not infallible: if you capture a reference to the fields in another lambda which then | |
* gets stored and invoked later, there may still be unsafe multi-threaded access going on, so watch out for that. | |
* This is just a guard rail that makes it harder to slip up. | |
* | |
* Example: | |
* | |
*``` | |
* private val state = Locker(object { var count: Int }) | |
* val current = state.locked { count++ } | |
* ``` | |
* | |
* **IMPORTANT:** The above short syntax relies heavily on Kotlin's type inference. In particular, the type of the | |
* `Locker` that's parameterised by an anonymous object is non-denotable and thus you _cannot_ write an explicit | |
* type for it. An attempt to do so will cause the lambdas to break. If this matters (e.g. you want to pass the box | |
* as a parameter), just define a named class instead of using an anonymous object. | |
* | |
* @param content The object to take ownership of and synchronize on. | |
*/ | |
public class Locker<out T>(content: T) { | |
/** @suppress */ | |
@JvmSynthetic | |
@PublishedApi | |
internal val __unlocked: T = content | |
/** | |
* Holds the lock whilst executing the block as an extension function on type [T]. | |
* | |
* @param reentrancy If false and the current thread already holds the lock, throws [IllegalStateException] | |
* with a message stating that "You may not call back into this object". This method is useful if you're invoking a | |
* user supplied callback and want to ensure the user doesn't re-invoke methods on your class whilst you're in | |
* the middle of processing a previous call. Defaults to 'true', meaning re-entrancy is allowed (the Java default). | |
*/ | |
@Suppress("DEPRECATION") | |
@OptIn(ExperimentalContracts::class) | |
public inline fun <R> locked(reentrancy: Boolean = true, block: T.() -> R): R { | |
contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } | |
if (!reentrancy && isLocked) throw IllegalStateException("You may not call back into this object.") | |
return synchronized(__unlocked as Any) { __unlocked.block() } | |
} | |
/** | |
* Returns true if the current thread holds the lock i.e. is inside a [locked] block. | |
*/ | |
public inline val isLocked: Boolean get() = Thread.holdsLock(__unlocked) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment