Skip to content

Commit

Permalink
ThreadLocalElement: stylistic updates to guide and docs
Browse files Browse the repository at this point in the history
  • Loading branch information
elizarov committed Aug 22, 2018
1 parent 91e7420 commit a86e186
Show file tree
Hide file tree
Showing 5 changed files with 63 additions and 49 deletions.
45 changes: 26 additions & 19 deletions core/kotlinx-coroutines-core/src/ThreadContextElement.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import kotlin.coroutines.experimental.*
* every time the coroutine with this element in the context is resumed on a thread.
*
* Implementations of this interface define a type [S] of the thread-local state that they need to store on
* resume of a coroutine and restore later on suspend and the infrastructure provides the corresponding storage.
* resume of a coroutine and restore later on suspend. The infrastructure provides the corresponding storage.
*
* Example usage looks like this:
*
Expand Down Expand Up @@ -42,9 +42,12 @@ import kotlin.coroutines.experimental.*
* // Usage
* launch(UI + CoroutineName("Progress bar coroutine")) { ... }
* ```
* Every time launched coroutine is executed, UI thread name will be updated to "UI thread original name # Progress bar coroutine"
*
* Note that for raw [ThreadLocal]s [asContextElement] factory should be used without any intermediate [ThreadContextElement] implementations
* Every time this coroutine is resumed on a thread, UI thread name is updated to
* "UI thread original name # Progress bar coroutine" and the thread name is restored to the original one when
* this coroutine suspends.
*
* To use [ThreadLocal] variable within the coroutine use [ThreadLocal.asContextElement][asContextElement] function.
*/
public interface ThreadContextElement<S> : CoroutineContext.Element {
/**
Expand All @@ -71,39 +74,43 @@ public interface ThreadContextElement<S> : CoroutineContext.Element {
}

/**
* Wraps [ThreadLocal] into [ThreadContextElement]. Resulting [ThreadContextElement] will
* maintain given [ThreadLocal] value for coroutine not depending on actual thread it's run on.
* By default [ThreadLocal.get] is used as a initial value for the element, but it can be overridden with [initialValue] parameter.
* Wraps [ThreadLocal] into [ThreadContextElement]. The resulting [ThreadContextElement]
* maintains the given [value] of the given [ThreadLocal] for coroutine regardless of the actual thread its is resumed on.
* By default [ThreadLocal.get] is used as a value for the thread-local variable, but it can be overridden with [value] parameter.
*
* Example usage looks like this:
*
* ```
* val myThreadLocal = ThreadLocal<String?>()
* ...
* println(myThreadLocal.get()) // Will print "null"
* println(myThreadLocal.get()) // Prints "null"
* launch(CommonPool + myThreadLocal.asContextElement(initialValue = "foo")) {
* println(myThreadLocal.get()) // Will print "foo"
* println(myThreadLocal.get()) // Prints "foo"
* withContext(UI) {
* println(myThreadLocal.get()) // Will print "foo", but it's UI thread
* println(myThreadLocal.get()) // Prints "foo", but it's on UI thread
* }
* }
*
* println(myThreadLocal.get()) // Will print "null"
* println(myThreadLocal.get()) // Prints "null"
* ```
*
* Note that context element doesn't track modifications of thread local, for example
* Note that the context element does not track modifications of the thread-local variable, for example:
*
* ```
* myThreadLocal.set("main")
* withContext(UI) {
* println(myThreadLocal.get()) // will print "main"
* println(myThreadLocal.get()) // Prints "main"
* myThreadLocal.set("UI")
* }
*
* println(myThreadLocal.get()) // will print "main", not "UI"
* println(myThreadLocal.get()) // Prints "main", not "UI"
* ```
*
* For modifications mutable boxes should be used instead
* Use `withContext` to update the corresponding thread-local variable to a different value, for example:
*
* ```
* withContext(myThreadLocal.asContextElement("foo")) {
* println(myThreadLocal.get()) // Prints "foo"
* }
* ```
*/
public fun <T> ThreadLocal<T>.asContextElement(initialValue: T = get()): ThreadContextElement<T> {
return ThreadLocalElement(initialValue, this)
}
public fun <T> ThreadLocal<T>.asContextElement(value: T = get()): ThreadContextElement<T> =
ThreadLocalElement(value, this)
16 changes: 11 additions & 5 deletions core/kotlinx-coroutines-core/src/internal/ThreadContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -89,28 +89,34 @@ internal fun restoreThreadContext(context: CoroutineContext, oldState: Any?) {
}
}

internal class ThreadLocalElement<T>(private val data: T, private val threadLocal: ThreadLocal<T>) : ThreadContextElement<T> {
private data class Key(private val tl: ThreadLocal<*>) : CoroutineContext.Key<ThreadLocalElement<*>>
// top-level data class for a nicer out-of-the-box toString representation and class name
private data class ThreadLocalKey(private val threadLocal: ThreadLocal<*>) : CoroutineContext.Key<ThreadLocalElement<*>>

override val key: CoroutineContext.Key<*> = Key(threadLocal)
internal 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 {
val oldState = threadLocal.get()
threadLocal.set(data)
threadLocal.set(value)
return oldState
}

override fun restoreThreadContext(context: CoroutineContext, oldState: T) {
threadLocal.set(oldState)
}

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

// this method is overridden to perform reference 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=$data, threadLocal = $threadLocal)"
override fun toString(): String = "ThreadLocal(value=$value, threadLocal = $threadLocal)"
}
3 changes: 1 addition & 2 deletions core/kotlinx-coroutines-core/test/ThreadLocalTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import kotlin.test.*

@Suppress("RedundantAsync")
class ThreadLocalTest : TestBase() {

private val stringThreadLocal = ThreadLocal<String?>()
private val intThreadLocal = ThreadLocal<Int?>()
private val executor = newFixedThreadPoolContext(1, "threadLocalTest")
Expand Down Expand Up @@ -55,7 +54,7 @@ class ThreadLocalTest : TestBase() {
intThreadLocal.set(314)

val deferred = async(CommonPool
+ intThreadLocal.asContextElement(initialValue = 239) + stringThreadLocal.asContextElement(initialValue = "pew")) {
+ intThreadLocal.asContextElement(value = 239) + stringThreadLocal.asContextElement(value = "pew")) {
assertEquals(239, intThreadLocal.get())
assertEquals("pew", stringThreadLocal.get())

Expand Down
6 changes: 2 additions & 4 deletions core/kotlinx-coroutines-core/test/guide/example-context-11.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,16 @@ package kotlinx.coroutines.experimental.guide.context11
import kotlinx.coroutines.experimental.*
import kotlin.coroutines.experimental.*

val threadLocal = ThreadLocal<String?>()
val threadLocal = ThreadLocal<String?>() // declare thread-local variable

fun main(args: Array<String>) = runBlocking<Unit> {
threadLocal.set("main")
println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")

val job = launch(CommonPool + threadLocal.asContextElement(initialValue = "launch"), start = CoroutineStart.UNDISPATCHED) {
val job = launch(CommonPool + threadLocal.asContextElement(value = "launch"), start = CoroutineStart.UNDISPATCHED) {
println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
yield()
println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
}

job.join()
println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
}
42 changes: 23 additions & 19 deletions coroutines-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ You need to add a dependency on `kotlinx-coroutines-core` module as explained
* [Parental responsibilities](#parental-responsibilities)
* [Naming coroutines for debugging](#naming-coroutines-for-debugging)
* [Cancellation via explicit job](#cancellation-via-explicit-job)
* [Thread-local data](#thread-local-data)
* [Thread-local data](#thread-local-data)
* [Channels](#channels)
* [Channel basics](#channel-basics)
* [Closing and iteration over channels](#closing-and-iteration-over-channels)
Expand Down Expand Up @@ -1262,36 +1262,36 @@ and cancel it when activity is destroyed. We cannot `join` them in the case of A
since it is synchronous, but this joining ability is useful when building backend services to ensure bounded
resource usage.

## Thread-local data
Sometimes it's very convenient to have an ability to pass some thread-local data, but for coroutines, which
are not bound to any particular thread, it's hard to achieve it manually without writing a lot of boilerplate.
### Thread-local data

Sometimes it is very convenient to have an ability to pass some thread-local data, but, for coroutines, which
are not bound to any particular thread, it is hard to achieve it manually without writing a lot of boilerplate.

For [`ThreadLocal`](https://docs.oracle.com/javase/8/docs/api/java/lang/ThreadLocal.html),
[asContextElement] is here for the rescue. It creates is an additional context element,
which remembers given `ThreadLocal` and restores it every time coroutine switches its context.
[asContextElement] is here for the rescue. It creates an additional context element,
which keep the value of the given `ThreadLocal` and restores it every time the coroutine switches its context.

It's easy to demonstrate it in action:
It is easy to demonstrate it in action:

<!--- INCLUDE
import kotlin.coroutines.experimental.*
-->

```kotlin
val threadLocal = ThreadLocal<String?>()
val threadLocal = ThreadLocal<String?>() // declare thread-local variable

fun main(args: Array<String>) = runBlocking<Unit> {
threadLocal.set("main")
println("Pre-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")

val job = launch(CommonPool + threadLocal.asContextElement(initialValue = "launch"), start = CoroutineStart.UNDISPATCHED) {
val job = launch(CommonPool + threadLocal.asContextElement(value = "launch"), start = CoroutineStart.UNDISPATCHED) {
println("Launch start, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
yield()
println("After yield, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
}

job.join()
println("Post-main, current thread: ${Thread.currentThread()}, thread local value: '${threadLocal.get()}'")
}
```
```

> You can get full code [here](core/kotlinx-coroutines-core/test/guide/example-context-11.kt)
Expand All @@ -1304,17 +1304,21 @@ After yield, current thread: Thread[ForkJoinPool.commonPool-worker-1 @coroutine#
Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
```

<!--- TEST FLEXIBLE_THREAD -->

Note that thread-local is restored properly, no matter on what thread coroutine is executed.
Note how thread-local value is restored properly, no matter on what thread the coroutine is executed.
`ThreadLocal` has first-class support and can be used with any primitive `kotlinx.corotuines` provides.
It has the only limitation: if thread-local is mutated, a new value is not propagated to the coroutine caller
(as context element cannot track all `ThreadLocal` object accesses), but is properly propagated to newly launched coroutines.
To workaround it, value can be stored in a mutable box like `class Counter(var i: Int)`
It has one key limitation: when thread-local is mutated, a new value is not propagated to the coroutine caller
(as context element cannot track all `ThreadLocal` object accesses) and updated value is lost on the next suspension.
Use [withContext] to update the value of the thread-local in a coroutine, see [asContextElement] for more details.

For advanced usage, for example for integration with logging MDC, transactional contexts or any other libraries
which use thread-locals for passing data, [ThreadContextElement] interface should be implemented.
Alternatively, a value can be stored in a mutable box like `class Counter(var i: Int)`, which is, in turn,
is stored in a thread-local variable. However, in this case you are fully responsible to synchronize
potentially concurrent modifications to the variable in this box.

<!--- TEST FLEXIBLE_THREAD -->
For advanced usage, for example for integration with logging MDC, transactional contexts or any other libraries
which internally use thread-locals for passing data, see documentation for [ThreadContextElement] interface
that should be implemented.

## Channels

Expand Down

0 comments on commit a86e186

Please sign in to comment.