From 85e693aba8c571ca26879f33c65f1def524790d6 Mon Sep 17 00:00:00 2001 From: Andrew Golovashevich Date: Tue, 9 Sep 2025 11:47:25 +0300 Subject: [PATCH] Docs for thread and minor fixes and improvements --- .../WrongCallerThreadException.kt | 10 ++ .../multitasking_0/threads/Thread.kt | 18 ++- .../threads/ThreadLocalMethods.kt | 58 +++++++++ .../threads/_FibersStackNode.kt | 21 ++++ .../threads/DrivenPlatformThread.kt | 3 +- .../multitasking_0/threads/ExternalThread.kt | 2 +- .../multitasking_0/threads/Thread.kt | 112 ++++++++++++++++-- .../threads/ThreadLocalMethods.kt | 16 +++ .../JavaVirtualThreadFiber.kt | 14 ++- .../multitasking_0/threads/Thread.kt | 83 ++++++++++++- 10 files changed, 306 insertions(+), 31 deletions(-) create mode 100644 src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/WrongCallerThreadException.kt create mode 100644 src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/ThreadLocalMethods.kt create mode 100644 src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/_FibersStackNode.kt create mode 100644 src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/ThreadLocalMethods.kt diff --git a/src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/WrongCallerThreadException.kt b/src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/WrongCallerThreadException.kt new file mode 100644 index 0000000..b180d34 --- /dev/null +++ b/src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/WrongCallerThreadException.kt @@ -0,0 +1,10 @@ +package ru.landgrafhomyak.multitasking_0 + +import kotlin.RuntimeException + +public class WrongCallerThreadException : RuntimeException { + public constructor() : super() + public constructor(message: String?) : super(message) + public constructor(message: String?, cause: Throwable?) : super(message, cause) + public constructor(cause: Throwable?) : super(cause) +} \ No newline at end of file diff --git a/src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/Thread.kt b/src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/Thread.kt index 8b1c5f1..c08f3ab 100644 --- a/src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/Thread.kt +++ b/src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/Thread.kt @@ -3,12 +3,24 @@ package ru.landgrafhomyak.multitasking_0.threads import ru.landgrafhomyak.multitasking_0.Fiber public expect class Thread { + /** + * Name of thread for debug purposes. + */ public val name: String override fun toString(): String - public var runningFiber: Fiber? - public companion object { - public fun currentThread(): Thread + /** + * Thread-local getter that returns object with access to some thread-local meta-information. + * + * Passing this object to another thread is not allowed and his members will throw error. + * + * + * ***Java's [virtual threads](https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html) note:*** + * If called inside [virtual thread used as fiber][ru.landgrafhomyak.multitasking_0.threads.impl.java_virtual_threads.JavaVirtualThreadFiber], + * it will return object associated with [Fiber.resumedOnThread] instead of actual thread. + * If a virtual thread is created another way, it will return its own descriptor. + */ + public val current: ThreadLocalMethods } } diff --git a/src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/ThreadLocalMethods.kt b/src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/ThreadLocalMethods.kt new file mode 100644 index 0000000..33c4b08 --- /dev/null +++ b/src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/ThreadLocalMethods.kt @@ -0,0 +1,58 @@ +package ru.landgrafhomyak.multitasking_0.threads + +import ru.landgrafhomyak.multitasking_0.Fiber +import ru.landgrafhomyak.multitasking_0.SingleThreadEventLoop +import ru.landgrafhomyak.multitasking_0.WrongCallerThreadException + +public expect sealed interface ThreadLocalMethods { + /** + * Returns descriptor of current thread. + * + * @throws WrongCallerThreadException if called on thread different that caller of [Thread.current]. + */ + public fun get(): Thread + + /** + * Returns descriptor of [fiber][Fiber] that is currently [running (or emulates running)][Fiber.resume] + * in this thread. + * + * If set, condition `Thread.current.runningFiber.resumedOnThread === Thread.current.get()` always true + * + * [Fiber][Fiber] implementations must control this property explicitly with [enterFiber()][ThreadLocalMethods.enterFiber] + * and [exitFiber()][ThreadLocalMethods.exitFiber] + * + * @throws WrongCallerThreadException if called on thread different that caller of [Thread.current]. + */ + public val runningFiber: Fiber? + + /** + * Informs thread that execution flow was switched to [fiber][Fiber]. + * + * @param fiberToEnter descriptor of fiber that is [going to run][Fiber.resume]. + * @param privateToken secret reference that should be used to call [exitFiber()][ThreadLocalMethods.exitFiber]. + * Need to avoid clearing [information about running fiber][ThreadLocalMethods.runningFiber] + * by anyone except implementation of this fiber. + * @return [fiber][Fiber] in which this function was called or `null` if it was called in thread. + * + * @throws WrongCallerThreadException if called on thread different that caller of [Thread.current]. + */ + public fun enterFiber(fiberToEnter: Fiber, privateToken: Any): Fiber? + + /** + * Informs thread that execution flow was returned from [fiber][Fiber]. + * + * @param fiberToExit descriptor of fiber that is [going to return execution flow][Fiber.yield]. + * @param privateToken secret reference that was passed to [enterFiber()][ThreadLocalMethods.exitFiber]. + * Used to check that this function called by same caller as [enterFiber()][ThreadLocalMethods.exitFiber]. + * @param fiberToRestore return value of corresponding [enterFiber()][ThreadLocalMethods.exitFiber] call. + * + * @throws IllegalStateException if [fiberToExit] or [fiberToRestore] doesn't match. + * @throws IllegalArgumentException if [privateToken] doesn't match. + * @throws WrongCallerThreadException if called on thread different that caller of [Thread.current]. + */ + public fun exitFiber(fiberToExit: Fiber, privateToken: Any, fiberToRestore: Fiber?) + + public val runningEventLoop: SingleThreadEventLoop? + public fun setEventLoop(eventLoop: SingleThreadEventLoop, privateToken: Any) + public fun clearEventLoop(eventLoop: SingleThreadEventLoop, privateToken: Any) +} \ No newline at end of file diff --git a/src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/_FibersStackNode.kt b/src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/_FibersStackNode.kt new file mode 100644 index 0000000..0e07d0a --- /dev/null +++ b/src/commonMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/_FibersStackNode.kt @@ -0,0 +1,21 @@ +package ru.landgrafhomyak.multitasking_0.threads + +import kotlin.jvm.JvmField +import ru.landgrafhomyak.multitasking_0.Fiber + +internal class _FibersStackNode( + @JvmField + val caller: _FibersStackNode?, + @JvmField + val fiber: Fiber, + @JvmField + val privateToken: Any, +) { + fun assertExit(fiberToExit: Fiber, privateToken: Any, fiberToRestore: Fiber?) { + if (this.fiber !== fiberToExit || this.caller?.fiber !== fiberToRestore) + throw IllegalStateException("fiberToExit or fiberToRestore doesn't match") + + if (this.privateToken !== privateToken) + throw IllegalArgumentException("privateToken doesn't match") + } +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/DrivenPlatformThread.kt b/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/DrivenPlatformThread.kt index f047e4a..41a0c07 100644 --- a/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/DrivenPlatformThread.kt +++ b/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/DrivenPlatformThread.kt @@ -2,12 +2,11 @@ package ru.landgrafhomyak.multitasking_0.threads import java.util.concurrent.CountDownLatch import java.lang.Thread as jThread -import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @Suppress("JoinDeclarationAndAssignment") internal class DrivenPlatformThread : Thread { - override fun toString(): String = TODO() + override fun toString(): String = "" override val _nativeThread: jThread diff --git a/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/ExternalThread.kt b/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/ExternalThread.kt index 9427255..129d938 100644 --- a/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/ExternalThread.kt +++ b/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/ExternalThread.kt @@ -4,7 +4,7 @@ import java.lang.Thread as jThread import kotlin.concurrent.withLock internal class ExternalThread : Thread { - override fun toString(): String = TODO() + override fun toString(): String = "" override val _nativeThread: jThread diff --git a/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/Thread.kt b/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/Thread.kt index a8e624b..56b32cc 100644 --- a/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/Thread.kt +++ b/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/Thread.kt @@ -4,24 +4,38 @@ import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock import java.lang.Thread as jThread import ru.landgrafhomyak.multitasking_0.Fiber +import ru.landgrafhomyak.multitasking_0.SingleThreadEventLoop +import ru.landgrafhomyak.multitasking_0.WrongCallerThreadException @Suppress("AMBIGUOUS_EXPECTS", "ACTUAL_WITHOUT_EXPECT") public actual sealed class Thread { - public actual var runningFiber: Fiber? = null + private val _threadLocalMethods = this.ThreadLocalMethodsImpl() protected abstract val _nativeThread: jThread protected val _sync: ReentrantLock = ReentrantLock() protected var _isExplicitlyDestroyed: Boolean = false protected var _uncaughtException: Throwable? = null + actual abstract override fun toString(): String public actual val name: String - get() = this._nativeThread.name + get() { + this._sync.withLock { + if (this._isExplicitlyDestroyed) + throw IllegalStateException("Thread destroyed") + } + return this._nativeThread.name + } public actual val isDaemon: Boolean - get() = this._nativeThread.isDaemon - + get() { + this._sync.withLock { + if (this._isExplicitlyDestroyed) + throw IllegalStateException("Thread destroyed") + } + return this._nativeThread.isDaemon + } public actual val state: State get() { this._sync.withLock { @@ -30,10 +44,10 @@ public actual sealed class Thread { when (this._nativeThread.state) { jThread.State.NEW -> return State.NEW - jThread.State.RUNNABLE -> return State.NOT_FINISHED - jThread.State.BLOCKED -> return State.NOT_FINISHED - jThread.State.WAITING -> return State.NOT_FINISHED - jThread.State.TIMED_WAITING -> return State.NOT_FINISHED + jThread.State.RUNNABLE -> return State.STARTED + jThread.State.BLOCKED -> return State.STARTED + jThread.State.WAITING -> return State.STARTED + jThread.State.TIMED_WAITING -> return State.STARTED jThread.State.TERMINATED -> { if (this._uncaughtException == null) return State.FINISHED_SUCCESSFULLY @@ -47,6 +61,9 @@ public actual sealed class Thread { public actual val uncaughtException: Throwable get() { this._sync.withLock { + if (this._isExplicitlyDestroyed) + throw IllegalStateException("Thread destroyed") + val e = this._uncaughtException if (e != null) return e @@ -105,7 +122,7 @@ public actual sealed class Thread { public actual enum class State { NEW, - NOT_FINISHED, + STARTED, FINISHED_SUCCESSFULLY, FINISHED_WITH_ERROR, DESTROYED @@ -122,11 +139,80 @@ public actual sealed class Thread { internal val _tl_currentThread: ThreadLocal get() = CurrentThreadVariable @JvmStatic - public actual fun create(name: String, isDaemon: Boolean, routine: ThreadRoutine): Thread = - DrivenPlatformThread(name, isDaemon, routine) + public actual val current: ThreadLocalMethods + get() = this._tl_currentThread.get()?._threadLocalMethods ?: throw RuntimeException("This java thread can't be wrapped") @JvmStatic - public actual fun currentThread(): Thread = - this._tl_currentThread.get() ?: throw RuntimeException("This java thread can't be wrapped") + public actual fun create(name: String, isDaemon: Boolean, routine: ThreadRoutine): Thread = + DrivenPlatformThread(name, isDaemon, routine) + } + + + private inner class ThreadLocalMethodsImpl : ThreadLocalMethods { + private fun _assertThread() { + if (Thread._tl_currentThread.get() != this@Thread) + throw WrongCallerThreadException("Reference returned by 'Thread.current' must be used only in thread where it was produced") + } + + override fun get(): Thread { + this._assertThread() + return this@Thread + } + + private var _fibersStack: _FibersStackNode? = null + + override val runningFiber: Fiber? + get() { + this._assertThread() + return this._fibersStack?.fiber + } + + override fun enterFiber(fiberToEnter: Fiber, privateToken: Any): Fiber? { + this._assertThread() + val caller = this._fibersStack + this._fibersStack = _FibersStackNode(caller, fiberToEnter, privateToken) + return caller?.fiber + } + + override fun exitFiber(fiberToExit: Fiber, privateToken: Any, fiberToRestore: Fiber?) { + this._assertThread() + + val top = this._fibersStack + if (top == null) + throw IllegalStateException("There is no running fiber to exit") + + top.assertExit(fiberToExit, privateToken, fiberToRestore) + + this._fibersStack = top.caller + } + + private var _runningEventLoop: SingleThreadEventLoop? = null + private var _runningEventLoopToken: Any? = null + + override val runningEventLoop: SingleThreadEventLoop? + get() { + this._assertThread() + return this._runningEventLoop + } + + + override fun setEventLoop(eventLoop: SingleThreadEventLoop, privateToken: Any) { + this._assertThread() + if (this._runningEventLoop != null) + throw IllegalStateException("There is already a running event loop here") + this._runningEventLoop = eventLoop + this._runningEventLoopToken = privateToken + } + + override fun clearEventLoop(eventLoop: SingleThreadEventLoop, privateToken: Any) { + this._assertThread() + if (this._runningEventLoop !== eventLoop) + throw IllegalStateException("Currently another event loop is running") + if (this._runningEventLoopToken !== privateToken) + throw IllegalArgumentException("privateToken doesn't match") + + this._runningEventLoop = null + this._runningEventLoopToken = null + } } } \ No newline at end of file diff --git a/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/ThreadLocalMethods.kt b/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/ThreadLocalMethods.kt new file mode 100644 index 0000000..f4393a9 --- /dev/null +++ b/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/ThreadLocalMethods.kt @@ -0,0 +1,16 @@ +package ru.landgrafhomyak.multitasking_0.threads + +import ru.landgrafhomyak.multitasking_0.Fiber +import ru.landgrafhomyak.multitasking_0.SingleThreadEventLoop + +public actual sealed interface ThreadLocalMethods { + public actual fun get(): Thread + + public actual val runningFiber: Fiber? + public actual fun enterFiber(fiberToEnter: Fiber, privateToken: Any): Fiber? + public actual fun exitFiber(fiberToExit: Fiber, privateToken: Any, fiberToRestore: Fiber?) + + public actual val runningEventLoop: SingleThreadEventLoop? + public actual fun setEventLoop(eventLoop: SingleThreadEventLoop, privateToken: Any) + public actual fun clearEventLoop(eventLoop: SingleThreadEventLoop, privateToken: Any) +} \ No newline at end of file diff --git a/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/impl/java_virtual_threads/JavaVirtualThreadFiber.kt b/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/impl/java_virtual_threads/JavaVirtualThreadFiber.kt index b99dfb1..4e807cd 100644 --- a/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/impl/java_virtual_threads/JavaVirtualThreadFiber.kt +++ b/src/jvmMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/impl/java_virtual_threads/JavaVirtualThreadFiber.kt @@ -1,5 +1,6 @@ package ru.landgrafhomyak.multitasking_0.threads.impl.java_virtual_threads +import java.lang.Object as jObject import java.lang.Thread as jThread import java.util.concurrent.locks.Condition import java.util.concurrent.locks.ReentrantLock @@ -7,6 +8,7 @@ import kotlin.concurrent.withLock import ru.landgrafhomyak.multitasking_0.ExecutionState import ru.landgrafhomyak.multitasking_0.Fiber import ru.landgrafhomyak.multitasking_0.FiberRoutine +import ru.landgrafhomyak.multitasking_0.WrongCallerThreadException import ru.landgrafhomyak.multitasking_0.threads.Thread as wThread public class JavaVirtualThreadFiber : Fiber { @@ -64,7 +66,7 @@ public class JavaVirtualThreadFiber : Fiber { override fun yield() { if (jThread.currentThread() !== this._thisVThread) - TODO("err") + throw WrongCallerThreadException("yield() must be called only inside it's fiber") this._syncLock.withLock { when (this._state) { @@ -101,15 +103,15 @@ public class JavaVirtualThreadFiber : Fiber { ExecutionState.FINISHED_SUCCESSFULLY, ExecutionState.FINISHED_WITH_ERROR -> throw IllegalStateException("Fiber already finished") } - val currentThread = wThread.currentThread() - this._resumedOnThread = currentThread - this._resumedOnFiber = currentThread.runningFiber - currentThread.runningFiber = this + val token = jObject() + val currentThread = wThread.current + this._resumedOnThread = currentThread.get() + this._resumedOnFiber = currentThread.enterFiber(this, token) this._syncCond.signal() this._syncCond.await() - currentThread.runningFiber = this._resumedOnFiber + currentThread.exitFiber(this, token, this.resumedOnFiber) this._resumedOnThread = null this._resumedOnFiber = null } diff --git a/src/multithreadPlatformMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/Thread.kt b/src/multithreadPlatformMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/Thread.kt index fcd2542..65f88ea 100644 --- a/src/multithreadPlatformMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/Thread.kt +++ b/src/multithreadPlatformMain/kotlin/ru/landgrafhomyak/multitasking_0/threads/Thread.kt @@ -3,33 +3,104 @@ package ru.landgrafhomyak.multitasking_0.threads import kotlin.experimental.ExpectRefinement import ru.landgrafhomyak.multitasking_0.Fiber - // didn't work because AMBIGUOUS_EXPECTS @OptIn(ExperimentalMultiplatform::class) @ExpectRefinement public expect class Thread { + /** + * Name of thread for debug purposes. + * + * After [releaseResources()][Thread.releaseResources] become inaccessible. + */ public val name: String override fun toString(): String - public var runningFiber: Fiber? + /** + * Start execution of [routine][ThreadRoutine] passed to [create()][Thread.create]. + * + * Doesn't define when any resources used by underlying functionality (e.g., stack) must be allocated. + */ public fun start() + + /** + * Blocks caller thread or fiber until [releaseResources()][Thread.releaseResources] call allowed. + * + * Calling this method before thread [started][Thread.start] is error. + */ public fun join() - public val state: State - public val uncaughtException: Throwable + + /** + * Confirms that all resources used by underlying functionality are released. + * Can be called only once. + * Can be called before thread [started][Thread.start] or after thread [finishes][Thread.join]. + */ public fun releaseResources() + + /** + * State of thread. Always accessible. + */ + public val state: State + + /** + * Exception that wasn't uncaught by [routine][ThreadRoutine] and terminated thread. + * + * Accessible only in [FINISHED_WITH_ERROR][Thread.State.FINISHED_WITH_ERROR] state, + * in other [states][Thread.state] will throw error, even after [releaseResources()][Thread.releaseResources]. + */ + public val uncaughtException: Throwable + + /** + * Returns `false` if thread prevents process from being finished while thread is running. + * + * After [releaseResources()][Thread.releaseResources] become inaccessible. + */ public val isDaemon: Boolean public enum class State { + /** + * Thread [created][Thread.create] but not [started][Thread.start] yet. + */ NEW, - NOT_FINISHED, + /** + * Thread [started][Thread.start], but not finished yet. + */ + STARTED, + /** + * Thread finished ([routine][ThreadRoutine] returned) normally. + */ FINISHED_SUCCESSFULLY, + /** + * Thread [finished with error][Thread.uncaughtException], that wasn't caught by [routine][ThreadRoutine]. + */ FINISHED_WITH_ERROR, + /** + * All resources allocated by this [Thread] object are released and reference to descriptor can be lost. + */ DESTROYED } public companion object { + /** + * Thread-local getter that returns object with access to some thread-local meta-information. + * + * Passing this object to another thread is not allowed and his members will throw error. + * + * + * ***Java's [virtual threads](https://docs.oracle.com/en/java/javase/21/core/virtual-threads.html) note:*** + * If called inside [virtual thread used as fiber][ru.landgrafhomyak.multitasking_0.threads.impl.java_virtual_threads.JavaVirtualThreadFiber], + * it will return object associated with [Fiber.resumedOnThread] instead of actual thread. + * If a virtual thread is created another way, it will return its own descriptor. + */ + public val current: ThreadLocalMethods + + /** + * Creates new [thread][Thread] in [unstarted][Thread.State.NEW] state. + * + * @param name name of thread for debug purposes. + * @param isDaemon should thread prevent process from being finished while thread is running. + * @param routine + */ public fun create(name: String, isDaemon: Boolean, routine: ThreadRoutine): Thread - public fun currentThread(): Thread } }