Hello! Could somebody explain please, why this cod...
# coroutines
d
Hello! Could somebody explain please, why this code have a race condition
Copy code
import kotlinx.coroutines.*

val myThreadLocal = ThreadLocal.withInitial { "initial" }

fun main() = runBlocking(myThreadLocal.asContextElement()) {
    myThreadLocal.set("main")
		withContext(Dispatchers.Default + myThreadLocal.asContextElement("test")) {
  			println(myThreadLocal.get()) 
  			myThreadLocal.set("UI")
		}
        
        println(myThreadLocal.get()) 
}
Sometimes the result is test;initial - sometimes test;main Thank you
r
Per the documentation each thread has a local copy of
myThreadLocal
. Dispathers.Default is backed by default by a pool of threads.
Depending on which thread
withContext
resumes on it may or may not be the thread
runBlocking
was on.
d
According to asContextElement https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/as-context-element.html the value would be set per coroutine, not thread
r
Sorry, I'm not familiar with
asContextElement
. I think when
withContext
returns, it's resuming on a different thread which doesn't have modified ThreadLocal with
main
.
Try printing the thread name to verify
println("${Thread.currentThread().name}")
Also per the docs "By default ThreadLocal.get is used as a value for the thread-local variable, but it can be overridden with value parameter"
And you are creating two different
asContextElement
overrides of the thread local.
If you want it to be the same you should only have the first
asContextElement
But I could be wrong
d
Will check it, thank you!
r
I just did and it gets the same result 😕
switching to
withContext(EmptyCoroutineContext)
is consistent. It seems that switching threads or dispatchers doesn't keep the same
ThreadLocal
override.
They even show that in the docs
Copy code
myThreadLocal.set("main")
withContext(Dispatchers.Main) {
  println(myThreadLocal.get()) // Prints "main"
  myThreadLocal.set("UI")
}
println(myThreadLocal.get()) // Prints "main", not "UI"
d
for me docs look like very bad, error-prone and hard to understand. Maybe somebody from JetBrains could help
r
Yep it is confusion. The last example especially
Copy code
val tl = ThreadLocal.withInitial { "initial" }

runBlocking {
  println(tl.get()) // Will print "initial"
  // Change context
  withContext(tl.asContextElement("modified")) {
    println(tl.get()) // Will print "modified"
  }
  // Context is changed again
   println(tl.get()) // <- WARN: can print either "modified" or "initial"
}
In my testing I wasn't able to get the last one to print "modified".
If the context changes dispatcher it seems to create a new instance of
ThreadContextElement
and modifications are not propagated. Is there something specific you are trying to do?
d
I’m trying to understand, how threadlicalelement works to write business logic using coroutines. Tried to use playground and docs, with maybe I misunderstood the concept
Still not clear how it works(Anyway thank you for trying help me
r
What I'm asking is why do you need a ThreadLocal? What use case do you have for it? There may be a more Kotlin coroutine idiomatic way to achieve it.
d
I need it for libraries that rely on it’s value
I think the issue relates to withContext, so depending on the thread that is used the coroutine thread local value will be set or not set
r
d
will check it, thank you!
Copy code
import kotlinx.coroutines.*
import kotlin.coroutines.*

val myThreadLocal = ThreadLocal.withInitial { "initial" }

data class ThreadLocalKey(private val threadLocal: ThreadLocal<*>) : CoroutineContext.Key<ThreadLocalElement<*>>

class ThreadLocalElement<T>(
    private val value: T,
    private val threadLocal: ThreadLocal<T>
) : ThreadContextElement<T> {
    override val key: CoroutineContext.Key<*> = ThreadLocalKey(threadLocal)

    override fun updateThreadContext(context: CoroutineContext): T {
        println("I'm updated")
        val oldState = threadLocal.get()
        threadLocal.set(value)
        return oldState
    }

    override fun restoreThreadContext(context: CoroutineContext, oldState: T) {
        println("I'm restored")
        threadLocal.set(oldState)
    }

    // this method is overridden to perform value comparison (==) on key
    override fun minusKey(key: CoroutineContext.Key<*>): CoroutineContext {
        return if (this.key == key) EmptyCoroutineContext else this
    }

    // this method is overridden to perform value comparison (==) on key
    public override operator fun <E : CoroutineContext.Element> get(key: CoroutineContext.Key<E>): E? =
        @Suppress("UNCHECKED_CAST")
        if (this.key == key) this as E else null

    override fun toString(): String = "ThreadLocal(value=$value, threadLocal = $threadLocal)"
}

fun main() = runBlocking(ThreadLocalElement(value = "initial", threadLocal = myThreadLocal)) {
    myThreadLocal.set("main")
    println(coroutineContext)
		withContext(Dispatchers.Default) {
            println(coroutineContext)
  			println(myThreadLocal.get()) 
  			myThreadLocal.set("UI")
		}
        
        
        
        println(myThreadLocal.get()) 
}
According to this test I see restored I sometimes called, sometimes not, depending on the runtime
@Dmitry Khalanskiy [JB] , may I ask you for some support please
Copy code
myThreadLocal.set("main")
withContext(Dispatchers.Main) {
  println(myThreadLocal.get()) // Prints "main"
  myThreadLocal.set("UI")
}
println(myThreadLocal.get()) // Prints "main", not "UI"
It’s very unclear what this code from documentation explains(
d
This code explains the statement directly above it:
The context element does not track modifications of the thread-local variable
So, changing a thread-local variable in a child coroutine will not propagate this change back to the parent coroutine.