-
Notifications
You must be signed in to change notification settings - Fork 61
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Thread parking for Kotlin/Common #498
base: master
Are you sure you want to change the base?
Conversation
atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/parking/KThread.kt
Outdated
Show resolved
Hide resolved
atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/parking/JvmParkingDelegator.kt
Outdated
Show resolved
Hide resolved
atomicfu/src/jvmMain/kotlin/kotlinx/atomicfu/parking/JvmParkingDelegator.kt
Outdated
Show resolved
Hide resolved
atomicfu/src/androidNative32BitMain/kotlin/kotlinx/atomicfu/parking/PosixParkingDelegator.kt
Outdated
Show resolved
Hide resolved
atomicfu/src/commonMain/kotlin/kotlinx/atomicfu/parking/ParkingDelegator.kt
Outdated
Show resolved
Hide resolved
|
||
override fun createRef(): Long { | ||
thread = Thread.currentThread() | ||
atomicLong.set(0L) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems to me like the atomic long here is a protection against spurious wake-ups. Is that it? I don't understand the utility of this safety mechanism. Code using thread parking is often already structured in a way that takes spurious wake-ups into account.
If we do remove the safety mechanism, however, it means that JvmParkingDelegator
can be stateless and use Thread
as the reference that's passed around. That, in turn, means that there's no need to create a parking delegator instance per thread parker.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have made the it stateless. However I do still check the spurious wakeups to have similar semantics to the native implementation. Why native needs this check has to do with memory deallocation (which is discussed further there)
actual override fun wake(ref: Any) { | ||
if (ref !is ParkingData) throw IllegalArgumentException("ParkingDelegator got incompatible parking object") | ||
pthread_mutex_lock(ref.mut) | ||
ref.wake.value = true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ditto: if this is only a protection against spurious wake-ups, it's more efficient to omit it, because it's likely that the calling code can easily add the same logic on top of the provided primitive, and it will usually be able to do that without creating extra mutable state (like the AtomicBoolean
here).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the case for native, the calling code should call the destroyRef function to deallocate. There can exists a race condition where:
- thread B (unparker) calls unpark and changes the state to free.
- thread A (parked) spuriously wakes up and, since the state changed to free, asumes it was woken up by a wake call. And continues to deallocate ParkingData.
- thread B continues to make the actual wake call, however ParkingData is destroyed.
The current parking implementation expects that when woken up there has been a wake call and therefore the reference will not be used anymore. So far I have not seen an easy way to solve this without protecting spurious wakeups, and since parking threads is anyways "expensive", I don't think it can hurt to leave it here?
WDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok, I understand now that having this shared mutable state in some form is unavoidable, thanks! Yes, I agree that with the current implementation of ThreadParker
, this change will lead to a data race. I think this can be avoided by reworking ThreadParker
to only have one piece of atomic state (atomic<FREE | UNPARKED | ParkingData>
), but I'm not sure if it would lead to anything useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Even with a single atomic state it seems difficult to me since we would still have to deallocate the native pthread_cond references. If we let the unparking thread deal with deallocation there exists a different race where the unpark call happens after the state change of park but before the actual delegator.wait(...)
call.
atomicfu/src/androidNative64BitMain/kotlin/kotlinx/atomicfu/parking/PosixParkingDelegator.kt
Outdated
Show resolved
Hide resolved
atomicfu/src/androidNative32BitMain/kotlin/kotlinx/atomicfu/parking/PosixParkingDelegator.kt
Show resolved
Hide resolved
atomicfu/src/androidNative32BitMain/kotlin/kotlinx/atomicfu/parking/PosixParkingDelegator.kt
Outdated
Show resolved
Hide resolved
} | ||
} | ||
private val thisKthread = KThread() | ||
actual fun currentThreadId(): Long = 1 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we do decide to avoid spurious wake-ups, this implementation is incorrect. But in any case, I think it's also useless to provide it: I'm not sure what anyone using thread synchronization on single-threaded runtimes even expects. As part of the mutex implementation, this particular implementation makes sense, but not for other primitives.
Proposal: create a source set combining JVM and Native (in kotlinx.coroutines
, we call it concurrent
), and instead of providing parking in common code, only provide it in concurrent
. Also, with this source set, it's easier to share tests between jvmTest
and nativeTest
: for most of them, it seems possible to put them in concurrentTest
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good suggestion! Added the sourceSet.
About the tests, spawning threads on Jvm and Native have different API's correct? Or would you suggest creating an expect/actual constructinon for thread manipulation?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do expect
/actual
in our test source sets all the time, and it works without issues: https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/concurrent/test/ConcurrentTestUtilities.common.kt.
… Commonizing is difficult due to absence of common threading api.
…arking behaviour on jvm and native.
b82d7aa
to
22d9ca4
Compare
STATE_FREE -> { | ||
if (!state.compareAndSet(currentState, STATE_UNPARKED)) continue | ||
return | ||
} | ||
|
||
STATE_PARKED -> { | ||
if (!state.compareAndSet(currentState, STATE_FREE)) continue | ||
delegator.wake(atomicRef.value!!) | ||
return | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Minor, style, subjective:
STATE_FREE -> { | |
if (!state.compareAndSet(currentState, STATE_UNPARKED)) continue | |
return | |
} | |
STATE_PARKED -> { | |
if (!state.compareAndSet(currentState, STATE_FREE)) continue | |
delegator.wake(atomicRef.value!!) | |
return | |
} | |
STATE_FREE -> if (state.compareAndSet(currentState, STATE_UNPARKED)) return | |
STATE_PARKED -> if (state.compareAndSet(currentState, STATE_FREE)) { | |
delegator.wake(atomicRef.value!!) | |
return | |
} |
If you like this option, the same trick can be used to simplify the code above.
Allows to pause and resume thread execution. On native platforms it is based on pthread_cond_wait, on JVM it uses LockSupport.
Threads can be pre unparked (calling unpark before park cancels the parking operation). And thread can be parker with a timeout.
Suspend the current thread by calling:
Parker.park()
Resume a thread by calling:
Parker.unpark(thread)
Get current thread reference by calling:
Parking with timout of 500 nano seconds: