Introduce ThreadLocal.asContextElement()
* Move implementation to internal package
* Add guide section
diff --git a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt
index 6307e06..500d3de 100644
--- a/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt
+++ b/binary-compatibility-validator/reference-public-api/kotlinx-coroutines-core.txt
@@ -446,6 +446,11 @@
public static fun plus (Lkotlinx/coroutines/experimental/ThreadContextElement;Lkotlin/coroutines/experimental/CoroutineContext;)Lkotlin/coroutines/experimental/CoroutineContext;
}
+public final class kotlinx/coroutines/experimental/ThreadContextElementKt {
+ public static final fun asContextElement (Ljava/lang/ThreadLocal;Ljava/lang/Object;)Lkotlinx/coroutines/experimental/ThreadContextElement;
+ public static synthetic fun asContextElement$default (Ljava/lang/ThreadLocal;Ljava/lang/Object;ILjava/lang/Object;)Lkotlinx/coroutines/experimental/ThreadContextElement;
+}
+
public final class kotlinx/coroutines/experimental/ThreadPoolDispatcher : kotlinx/coroutines/experimental/ExecutorCoroutineDispatcherBase {
public fun close ()V
public fun getExecutor ()Ljava/util/concurrent/Executor;
diff --git a/core/kotlinx-coroutines-core/src/CoroutineContext.kt b/core/kotlinx-coroutines-core/src/CoroutineContext.kt
index 247ba50..2fcd014 100644
--- a/core/kotlinx-coroutines-core/src/CoroutineContext.kt
+++ b/core/kotlinx-coroutines-core/src/CoroutineContext.kt
@@ -4,7 +4,6 @@
package kotlinx.coroutines.experimental
-import java.util.*
import kotlinx.coroutines.experimental.internal.*
import kotlinx.coroutines.experimental.scheduling.*
import java.util.concurrent.atomic.*
diff --git a/core/kotlinx-coroutines-core/src/ThreadContextElement.kt b/core/kotlinx-coroutines-core/src/ThreadContextElement.kt
index 140c9c3..b43497d 100644
--- a/core/kotlinx-coroutines-core/src/ThreadContextElement.kt
+++ b/core/kotlinx-coroutines-core/src/ThreadContextElement.kt
@@ -12,36 +12,42 @@
* 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:
*
* ```
- * // declare thread local variable holding MyData
- * private val myThreadLocal = ThreadLocal<MyData?>()
- *
- * // declare context element holding MyData
- * class MyElement(val data: MyData) : ThreadContextElement<MyData?> {
+ * // Appends "name" of a coroutine to a current thread name when coroutine is executed
+ * class CoroutineName(val name: String) : ThreadContextElement<String> {
* // declare companion object for a key of this element in coroutine context
- * companion object Key : CoroutineContext.Key<MyElement>
+ * companion object Key : CoroutineContext.Key<CoroutineName>
*
* // provide the key of the corresponding context element
- * override val key: CoroutineContext.Key<MyElement>
+ * override val key: CoroutineContext.Key<CoroutineName>
* get() = Key
*
* // this is invoked before coroutine is resumed on current thread
- * override fun updateThreadContext(context: CoroutineContext): MyData? {
- * val oldState = myThreadLocal.get()
- * myThreadLocal.set(data)
- * return oldState
+ * override fun updateThreadContext(context: CoroutineContext): String {
+ * val previousName = Thread.currentThread().name
+ * Thread.currentThread().name = "$previousName # $name"
+ * return previousName
* }
*
* // this is invoked after coroutine has suspended on current thread
- * override fun restoreThreadContext(context: CoroutineContext, oldState: MyData?) {
- * myThreadLocal.set(oldState)
+ * override fun restoreThreadContext(context: CoroutineContext, oldState: String) {
+ * Thread.currentThread().name = oldState
* }
* }
+ *
+ * // Usage
+ * launch(UI + CoroutineName("Progress bar coroutine")) { ... }
* ```
+ *
+ * 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 {
/**
@@ -67,87 +73,44 @@
public fun restoreThreadContext(context: CoroutineContext, oldState: S)
}
-private val ZERO = Symbol("ZERO")
-
-// Used when there are >= 2 active elements in the context
-private class ThreadState(val context: CoroutineContext, n: Int) {
- private var a = arrayOfNulls<Any>(n)
- private var i = 0
-
- fun append(value: Any?) { a[i++] = value }
- fun take() = a[i++]
- fun start() { i = 0 }
-}
-
-// Counts ThreadContextElements in the context
-// Any? here is Int | ThreadContextElement (when count is one)
-private val countAll =
- fun (countOrElement: Any?, element: CoroutineContext.Element): Any? {
- if (element is ThreadContextElement<*>) {
- val inCount = countOrElement as? Int ?: 1
- return if (inCount == 0) element else inCount + 1
- }
- return countOrElement
- }
-
-// Find one (first) ThreadContextElement in the context, it is used when we know there is exactly one
-private val findOne =
- fun (found: ThreadContextElement<*>?, element: CoroutineContext.Element): ThreadContextElement<*>? {
- if (found != null) return found
- return element as? ThreadContextElement<*>
- }
-
-// Updates state for ThreadContextElements in the context using the given ThreadState
-private val updateState =
- fun (state: ThreadState, element: CoroutineContext.Element): ThreadState {
- if (element is ThreadContextElement<*>) {
- state.append(element.updateThreadContext(state.context))
- }
- return state
- }
-
-// Restores state for all ThreadContextElements in the context from the given ThreadState
-private val restoreState =
- fun (state: ThreadState, element: CoroutineContext.Element): ThreadState {
- @Suppress("UNCHECKED_CAST")
- if (element is ThreadContextElement<*>) {
- (element as ThreadContextElement<Any?>).restoreThreadContext(state.context, state.take())
- }
- return state
- }
-
-internal fun updateThreadContext(context: CoroutineContext): Any? {
- val count = context.fold(0, countAll)
- @Suppress("IMPLICIT_BOXING_IN_IDENTITY_EQUALS")
- return when {
- count === 0 -> ZERO // very fast path when there are no active ThreadContextElements
- // ^^^ identity comparison for speed, we know zero always has the same identity
- count is Int -> {
- // slow path for multiple active ThreadContextElements, allocates ThreadState for multiple old values
- context.fold(ThreadState(context, count), updateState)
- }
- else -> {
- // fast path for one ThreadContextElement (no allocations, no additional context scan)
- @Suppress("UNCHECKED_CAST")
- val element = count as ThreadContextElement<Any?>
- element.updateThreadContext(context)
- }
- }
-}
-
-internal fun restoreThreadContext(context: CoroutineContext, oldState: Any?) {
- when {
- oldState === ZERO -> return // very fast path when there are no ThreadContextElements
- oldState is ThreadState -> {
- // slow path with multiple stored ThreadContextElements
- oldState.start()
- context.fold(oldState, restoreState)
- }
- else -> {
- // fast path for one ThreadContextElement, but need to find it
- @Suppress("UNCHECKED_CAST")
- val element = context.fold(null, findOne) as ThreadContextElement<Any?>
- element.restoreThreadContext(context, oldState)
- }
- }
-}
+/**
+ * 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()) // Prints "null"
+ * launch(CommonPool + myThreadLocal.asContextElement(initialValue = "foo")) {
+ * println(myThreadLocal.get()) // Prints "foo"
+ * withContext(UI) {
+ * println(myThreadLocal.get()) // Prints "foo", but it's on UI thread
+ * }
+ * }
+ * println(myThreadLocal.get()) // Prints "null"
+ * ```
+ *
+ * Note that the context element does not track modifications of the thread-local variable, for example:
+ *
+ * ```
+ * myThreadLocal.set("main")
+ * withContext(UI) {
+ * println(myThreadLocal.get()) // Prints "main"
+ * myThreadLocal.set("UI")
+ * }
+ * println(myThreadLocal.get()) // Prints "main", not "UI"
+ * ```
+ *
+ * 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(value: T = get()): ThreadContextElement<T> =
+ ThreadLocalElement(value, this)
diff --git a/core/kotlinx-coroutines-core/src/internal/ThreadContext.kt b/core/kotlinx-coroutines-core/src/internal/ThreadContext.kt
new file mode 100644
index 0000000..abee55b
--- /dev/null
+++ b/core/kotlinx-coroutines-core/src/internal/ThreadContext.kt
@@ -0,0 +1,122 @@
+package kotlinx.coroutines.experimental.internal
+
+import kotlinx.coroutines.experimental.*
+import kotlin.coroutines.experimental.*
+
+
+private val ZERO = Symbol("ZERO")
+
+// Used when there are >= 2 active elements in the context
+private class ThreadState(val context: CoroutineContext, n: Int) {
+ private var a = arrayOfNulls<Any>(n)
+ private var i = 0
+
+ fun append(value: Any?) { a[i++] = value }
+ fun take() = a[i++]
+ fun start() { i = 0 }
+}
+
+// Counts ThreadContextElements in the context
+// Any? here is Int | ThreadContextElement (when count is one)
+private val countAll =
+ fun (countOrElement: Any?, element: CoroutineContext.Element): Any? {
+ if (element is ThreadContextElement<*>) {
+ val inCount = countOrElement as? Int ?: 1
+ return if (inCount == 0) element else inCount + 1
+ }
+ return countOrElement
+ }
+
+// Find one (first) ThreadContextElement in the context, it is used when we know there is exactly one
+private val findOne =
+ fun (found: ThreadContextElement<*>?, element: CoroutineContext.Element): ThreadContextElement<*>? {
+ if (found != null) return found
+ return element as? ThreadContextElement<*>
+ }
+
+// Updates state for ThreadContextElements in the context using the given ThreadState
+private val updateState =
+ fun (state: ThreadState, element: CoroutineContext.Element): ThreadState {
+ if (element is ThreadContextElement<*>) {
+ state.append(element.updateThreadContext(state.context))
+ }
+ return state
+ }
+
+// Restores state for all ThreadContextElements in the context from the given ThreadState
+private val restoreState =
+ fun (state: ThreadState, element: CoroutineContext.Element): ThreadState {
+ @Suppress("UNCHECKED_CAST")
+ if (element is ThreadContextElement<*>) {
+ (element as ThreadContextElement<Any?>).restoreThreadContext(state.context, state.take())
+ }
+ return state
+ }
+
+internal fun updateThreadContext(context: CoroutineContext): Any? {
+ val count = context.fold(0, countAll)
+ @Suppress("IMPLICIT_BOXING_IN_IDENTITY_EQUALS")
+ return when {
+ count === 0 -> ZERO // very fast path when there are no active ThreadContextElements
+ // ^^^ identity comparison for speed, we know zero always has the same identity
+ count is Int -> {
+ // slow path for multiple active ThreadContextElements, allocates ThreadState for multiple old values
+ context.fold(ThreadState(context, count), updateState)
+ }
+ else -> {
+ // fast path for one ThreadContextElement (no allocations, no additional context scan)
+ @Suppress("UNCHECKED_CAST")
+ val element = count as ThreadContextElement<Any?>
+ element.updateThreadContext(context)
+ }
+ }
+}
+
+internal fun restoreThreadContext(context: CoroutineContext, oldState: Any?) {
+ when {
+ oldState === ZERO -> return // very fast path when there are no ThreadContextElements
+ oldState is ThreadState -> {
+ // slow path with multiple stored ThreadContextElements
+ oldState.start()
+ context.fold(oldState, restoreState)
+ }
+ else -> {
+ // fast path for one ThreadContextElement, but need to find it
+ @Suppress("UNCHECKED_CAST")
+ val element = context.fold(null, findOne) as ThreadContextElement<Any?>
+ element.restoreThreadContext(context, oldState)
+ }
+ }
+}
+
+// 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<*>>
+
+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(value)
+ return oldState
+ }
+
+ override fun restoreThreadContext(context: CoroutineContext, oldState: T) {
+ 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)"
+}
diff --git a/core/kotlinx-coroutines-core/test/ThreadContextElementTest.kt b/core/kotlinx-coroutines-core/test/ThreadContextElementTest.kt
index 0c670f2..a8b17d2 100644
--- a/core/kotlinx-coroutines-core/test/ThreadContextElementTest.kt
+++ b/core/kotlinx-coroutines-core/test/ThreadContextElementTest.kt
@@ -52,6 +52,39 @@
job.join()
assertNull(myThreadLocal.get())
}
+
+
+ @Test
+ fun testWithContext() = runTest {
+ expect(1)
+ newSingleThreadContext("withContext").use {
+ val data = MyData()
+ async(CommonPool + MyElement(data)) {
+ assertSame(data, myThreadLocal.get())
+ expect(2)
+
+ val newData = MyData()
+ async(it + MyElement(newData)) {
+ assertSame(newData, myThreadLocal.get())
+ expect(3)
+ }.await()
+
+ withContext(it + MyElement(newData)) {
+ assertSame(newData, myThreadLocal.get())
+ expect(4)
+ }
+
+ async(it) {
+ assertNull(myThreadLocal.get())
+ expect(5)
+ }.await()
+
+ expect(6)
+ }.await()
+ }
+
+ finish(7)
+ }
}
class MyData
diff --git a/core/kotlinx-coroutines-core/test/ThreadLocalTest.kt b/core/kotlinx-coroutines-core/test/ThreadLocalTest.kt
new file mode 100644
index 0000000..b932e75
--- /dev/null
+++ b/core/kotlinx-coroutines-core/test/ThreadLocalTest.kt
@@ -0,0 +1,199 @@
+
+package kotlinx.coroutines.experimental
+
+import org.junit.*
+import org.junit.Test
+import kotlin.coroutines.experimental.*
+import kotlin.test.*
+
+@Suppress("RedundantAsync")
+class ThreadLocalTest : TestBase() {
+ private val stringThreadLocal = ThreadLocal<String?>()
+ private val intThreadLocal = ThreadLocal<Int?>()
+ private val executor = newFixedThreadPoolContext(1, "threadLocalTest")
+
+ @After
+ fun tearDown() {
+ executor.close()
+ }
+
+ @Test
+ fun testThreadLocal() = runTest {
+ assertNull(stringThreadLocal.get())
+ val deferred = async(CommonPool + stringThreadLocal.asContextElement("value")) {
+ assertEquals("value", stringThreadLocal.get())
+ withContext(executor) {
+ assertEquals("value", stringThreadLocal.get())
+ }
+ assertEquals("value", stringThreadLocal.get())
+ }
+
+ assertNull(stringThreadLocal.get())
+ deferred.await()
+ assertNull(stringThreadLocal.get())
+ }
+
+ @Test
+ fun testThreadLocalInitialValue() = runTest {
+ intThreadLocal.set(42)
+ val deferred = async(CommonPool + intThreadLocal.asContextElement(239)) {
+ assertEquals(239, intThreadLocal.get())
+ withContext(executor) {
+ assertEquals(239, intThreadLocal.get())
+ }
+ assertEquals(239, intThreadLocal.get())
+ }
+
+ deferred.await()
+ assertEquals(42, intThreadLocal.get())
+ }
+
+ @Test
+ fun testMultipleThreadLocals() = runTest {
+ stringThreadLocal.set("test")
+ intThreadLocal.set(314)
+
+ val deferred = async(CommonPool
+ + intThreadLocal.asContextElement(value = 239) + stringThreadLocal.asContextElement(value = "pew")) {
+ assertEquals(239, intThreadLocal.get())
+ assertEquals("pew", stringThreadLocal.get())
+
+ withContext(executor) {
+ assertEquals(239, intThreadLocal.get())
+ assertEquals("pew", stringThreadLocal.get())
+ }
+
+ assertEquals(239, intThreadLocal.get())
+ assertEquals("pew", stringThreadLocal.get())
+ }
+
+ deferred.await()
+ assertEquals(314, intThreadLocal.get())
+ assertEquals("test", stringThreadLocal.get())
+ }
+
+ @Test
+ fun testConflictingThreadLocals() = runTest {
+ intThreadLocal.set(42)
+
+ val deferred = async(CommonPool
+ + intThreadLocal.asContextElement(1)) {
+ assertEquals(1, intThreadLocal.get())
+
+ withContext(executor + intThreadLocal.asContextElement(42)) {
+ assertEquals(42, intThreadLocal.get())
+ }
+
+ assertEquals(1, intThreadLocal.get())
+
+ val deferred = async(coroutineContext + intThreadLocal.asContextElement(53)) {
+ assertEquals(53, intThreadLocal.get())
+ }
+
+ deferred.await()
+ assertEquals(1, intThreadLocal.get())
+
+ val deferred2 = async(executor) {
+ assertNull(intThreadLocal.get())
+ }
+
+ deferred2.await()
+ assertEquals(1, intThreadLocal.get())
+ }
+
+ deferred.await()
+ assertEquals(42, intThreadLocal.get())
+ }
+
+ @Test
+ fun testThreadLocalModification() = runTest {
+ stringThreadLocal.set("main")
+
+ val deferred = async(CommonPool
+ + stringThreadLocal.asContextElement("initial")) {
+ assertEquals("initial", stringThreadLocal.get())
+
+ stringThreadLocal.set("overridden") // <- this value is not reflected in the context, so it's not restored
+
+ withContext(executor + stringThreadLocal.asContextElement("ctx")) {
+ assertEquals("ctx", stringThreadLocal.get())
+ }
+
+ val deferred = async(coroutineContext + stringThreadLocal.asContextElement("async")) {
+ assertEquals("async", stringThreadLocal.get())
+ }
+
+ deferred.await()
+ assertEquals("initial", stringThreadLocal.get()) // <- not restored
+ }
+
+ deferred.await()
+ assertEquals("main", stringThreadLocal.get())
+ }
+
+
+
+ private data class Counter(var cnt: Int)
+ private val myCounterLocal = ThreadLocal<Counter>()
+
+ @Test
+ fun testThreadLocalModificationMutableBox() = runTest {
+ myCounterLocal.set(Counter(42))
+
+ val deferred = async(CommonPool
+ + myCounterLocal.asContextElement(Counter(0))) {
+ assertEquals(0, myCounterLocal.get().cnt)
+
+ // Mutate
+ myCounterLocal.get().cnt = 71
+
+ withContext(executor + myCounterLocal.asContextElement(Counter(-1))) {
+ assertEquals(-1, myCounterLocal.get().cnt)
+ ++myCounterLocal.get().cnt
+ }
+
+ val deferred = async(coroutineContext + myCounterLocal.asContextElement(Counter(31))) {
+ assertEquals(31, myCounterLocal.get().cnt)
+ ++myCounterLocal.get().cnt
+ }
+
+ deferred.await()
+ assertEquals(71, myCounterLocal.get().cnt)
+ }
+
+ deferred.await()
+ assertEquals(42, myCounterLocal.get().cnt)
+ }
+
+ @Test
+ fun testWithContext() = runTest {
+ expect(1)
+ newSingleThreadContext("withContext").use {
+ val data = 42
+ async(CommonPool + intThreadLocal.asContextElement(42)) {
+
+ assertSame(data, intThreadLocal.get())
+ expect(2)
+
+ async(it + intThreadLocal.asContextElement(31)) {
+ assertEquals(31, intThreadLocal.get())
+ expect(3)
+ }.await()
+
+ withContext(it + intThreadLocal.asContextElement(2)) {
+ assertSame(2, intThreadLocal.get())
+ expect(4)
+ }
+
+ async(it) {
+ assertNull(intThreadLocal.get())
+ expect(5)
+ }.await()
+
+ expect(6)
+ }.await()
+ }
+
+ finish(7)
+ }
+}
diff --git a/core/kotlinx-coroutines-core/test/guide/example-context-11.kt b/core/kotlinx-coroutines-core/test/guide/example-context-11.kt
new file mode 100644
index 0000000..4d43911
--- /dev/null
+++ b/core/kotlinx-coroutines-core/test/guide/example-context-11.kt
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2016-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
+ */
+
+// This file was automatically generated from coroutines-guide.md by Knit tool. Do not edit.
+package kotlinx.coroutines.experimental.guide.context11
+
+import kotlinx.coroutines.experimental.*
+import kotlin.coroutines.experimental.*
+
+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(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()}'")
+}
diff --git a/core/kotlinx-coroutines-core/test/guide/test/GuideTest.kt b/core/kotlinx-coroutines-core/test/guide/test/GuideTest.kt
index ec527a6..82958a2 100644
--- a/core/kotlinx-coroutines-core/test/guide/test/GuideTest.kt
+++ b/core/kotlinx-coroutines-core/test/guide/test/GuideTest.kt
@@ -268,6 +268,16 @@
}
@Test
+ fun testKotlinxCoroutinesExperimentalGuideContext11() {
+ test("KotlinxCoroutinesExperimentalGuideContext11") { kotlinx.coroutines.experimental.guide.context11.main(emptyArray()) }.verifyLinesFlexibleThread(
+ "Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'",
+ "Launch start, current thread: Thread[main @coroutine#2,5,main], thread local value: 'launch'",
+ "After yield, current thread: Thread[ForkJoinPool.commonPool-worker-1 @coroutine#2,5,main], thread local value: 'launch'",
+ "Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'"
+ )
+ }
+
+ @Test
fun testKotlinxCoroutinesExperimentalGuideExceptions01() {
test("KotlinxCoroutinesExperimentalGuideExceptions01") { kotlinx.coroutines.experimental.guide.exceptions01.main(emptyArray()) }.verifyExceptions(
"Throwing exception from launch",
diff --git a/coroutines-guide.md b/coroutines-guide.md
index bfdbe8c..c1d8679 100644
--- a/coroutines-guide.md
+++ b/coroutines-guide.md
@@ -67,6 +67,7 @@
* [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)
* [Exception handling](#exception-handling)
* [Exception propagation](#exception-propagation)
* [CoroutineExceptionHandler](#coroutineexceptionhandler)
@@ -1266,6 +1267,64 @@
since it is synchronous, but this joining ability is useful when building backend services to ensure bounded
resource usage.
+### 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 an additional context element,
+which keep the value of the given `ThreadLocal` and restores it every time the coroutine switches its context.
+
+It is easy to demonstrate it in action:
+
+<!--- INCLUDE
+import kotlin.coroutines.experimental.*
+-->
+
+```kotlin
+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(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)
+
+The output of this example is:
+
+```text
+Pre-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
+Launch start, current thread: Thread[main @coroutine#2,5,main], thread local value: 'launch'
+After yield, current thread: Thread[ForkJoinPool.commonPool-worker-1 @coroutine#2,5,main], thread local value: 'launch'
+Post-main, current thread: Thread[main @coroutine#1,5,main], thread local value: 'main'
+```
+
+<!--- TEST FLEXIBLE_THREAD -->
+
+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 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.
+
+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.
+
+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.
+
## Exception handling
<!--- INCLUDE .*/example-exceptions-([0-9]+).kt
@@ -2776,6 +2835,8 @@
[newCoroutineContext]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/new-coroutine-context.html
[CoroutineName]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-coroutine-name/index.html
[Job()]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-job.html
+[asContextElement]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/java.lang.-thread-local/as-context-element.html
+[ThreadContextElement]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-thread-context-element/index.html
[CoroutineExceptionHandler]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-coroutine-exception-handler/index.html
[kotlin.coroutines.experimental.CoroutineContext.cancelChildren]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/kotlin.coroutines.experimental.-coroutine-context/cancel-children.html
[CompletableDeferred]: https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.experimental/-completable-deferred/index.html