@@ -9,6 +9,7 @@ import kotlinx.atomicfu.*
99import kotlinx.coroutines.internal.*
1010import kotlinx.coroutines.intrinsics.*
1111import kotlinx.coroutines.selects.*
12+ import kotlin.concurrent.Volatile
1213import kotlin.contracts.*
1314import kotlin.coroutines.*
1415import kotlin.coroutines.intrinsics.*
@@ -206,10 +207,124 @@ private class LazyStandaloneCoroutine(
206207}
207208
208209// Used by withContext when context changes, but dispatcher stays the same
209- internal expect class UndispatchedCoroutine <in T >(
210+ internal actual class UndispatchedCoroutine <in T >actual constructor (
210211 context : CoroutineContext ,
211212 uCont : Continuation <T >
212- ) : ScopeCoroutine<T>
213+ ) : ScopeCoroutine<T>(if (context[UndispatchedMarker ] == null) context + UndispatchedMarker else context, uCont) {
214+
215+ /* *
216+ * The state of [ThreadContextElement]s associated with the current undispatched coroutine.
217+ * It is stored in a thread local because this coroutine can be used concurrently in suspend-resume race scenario.
218+ * See the followin, boiled down example with inlined `withContinuationContext` body:
219+ * ```
220+ * val state = saveThreadContext(ctx)
221+ * try {
222+ * invokeSmthWithThisCoroutineAsCompletion() // Completion implies that 'afterResume' will be called
223+ * // COROUTINE_SUSPENDED is returned
224+ * } finally {
225+ * thisCoroutine().clearThreadContext() // Concurrently the "smth" could've been already resumed on a different thread
226+ * // and it also calls saveThreadContext and clearThreadContext
227+ * }
228+ * ```
229+ *
230+ * Usage note:
231+ *
232+ * This part of the code is performance-sensitive.
233+ * It is a well-established pattern to wrap various activities into system-specific undispatched
234+ * `withContext` for the sake of logging, MDC, tracing etc., meaning that there exists thousands of
235+ * undispatched coroutines.
236+ * Each access to Java's [ThreadLocal] leaves a footprint in the corresponding Thread's `ThreadLocalMap`
237+ * that is cleared automatically as soon as the associated thread-local (-> UndispatchedCoroutine) is garbage collected
238+ * when either the corresponding thread is GC'ed or it cleans up its stale entries on other TL accesses.
239+ * When such coroutines are promoted to old generation, `ThreadLocalMap`s become bloated and an arbitrary accesses to thread locals
240+ * start to consume significant amount of CPU because these maps are open-addressed and cleaned up incrementally on each access.
241+ * (You can read more about this effect as "GC nepotism").
242+ *
243+ * To avoid that, we attempt to narrow down the lifetime of this thread local as much as possible:
244+ * - It's never accessed when we are sure there are no thread context elements
245+ * - It's cleaned up via [ThreadLocal.remove] as soon as the coroutine is suspended or finished.
246+ */
247+ private val threadStateToRecover = ThreadLocal <Pair <CoroutineContext , Any ?>>()
248+
249+ /*
250+ * Indicates that a coroutine has at least one thread context element associated with it
251+ * and that 'threadStateToRecover' is going to be set in case of dispatchhing in order to preserve them.
252+ * Better than nullable thread-local for easier debugging.
253+ *
254+ * It is used as a performance optimization to avoid 'threadStateToRecover' initialization
255+ * (note: tl.get() initializes thread local),
256+ * and is prone to false-positives as it is never reset: otherwise
257+ * it may lead to logical data races between suspensions point where
258+ * coroutine is yet being suspended in one thread while already being resumed
259+ * in another.
260+ */
261+ @Volatile
262+ private var threadLocalIsSet = false
263+
264+ init {
265+ /*
266+ * This is a hack for a very specific case in #2930 unless #3253 is implemented.
267+ * 'ThreadLocalStressTest' covers this change properly.
268+ *
269+ * The scenario this change covers is the following:
270+ * 1) The coroutine is being started as plain non kotlinx.coroutines related suspend function,
271+ * e.g. `suspend fun main` or, more importantly, Ktor `SuspendFunGun`, that is invoking
272+ * `withContext(tlElement)` which creates `UndispatchedCoroutine`.
273+ * 2) It (original continuation) is then not wrapped into `DispatchedContinuation` via `intercept()`
274+ * and goes neither through `DC.run` nor through `resumeUndispatchedWith` that both
275+ * do thread context element tracking.
276+ * 3) So thread locals never got chance to get properly set up via `saveThreadContext`,
277+ * but when `withContext` finishes, it attempts to recover thread locals in its `afterResume`.
278+ *
279+ * Here we detect precisely this situation and properly setup context to recover later.
280+ *
281+ */
282+ if (uCont.context[ContinuationInterceptor ] !is CoroutineDispatcher ) {
283+ /*
284+ * We cannot just "read" the elements as there is no such API,
285+ * so we update-restore it immediately and use the intermediate value
286+ * as the initial state, leveraging the fact that thread context element
287+ * is idempotent and such situations are increasingly rare.
288+ */
289+ val values = updateThreadContext(context, null )
290+ restoreThreadContext(context, values)
291+ saveThreadContext(context, values)
292+ }
293+ }
294+
295+ fun saveThreadContext (context : CoroutineContext , oldValue : Any? ) {
296+ threadLocalIsSet = true // Specify that thread-local is touched at all
297+ threadStateToRecover.set(context to oldValue)
298+ }
299+
300+ fun clearThreadContext (): Boolean {
301+ return ! (threadLocalIsSet && threadStateToRecover.get() == null ).also {
302+ threadStateToRecover.remove()
303+ }
304+ }
305+
306+ override fun afterCompletionUndispatched () {
307+ clearThreadLocal()
308+ }
309+
310+ override fun afterResume (state : Any? ) {
311+ clearThreadLocal()
312+ // resume undispatched -- update context but stay on the same dispatcher
313+ val result = recoverResult(state, uCont)
314+ withContinuationContext(uCont, null ) {
315+ uCont.resumeWith(result)
316+ }
317+ }
318+
319+ private fun clearThreadLocal () {
320+ if (threadLocalIsSet) {
321+ threadStateToRecover.get()?.let { (ctx, value) ->
322+ restoreThreadContext(ctx, value)
323+ }
324+ threadStateToRecover.remove()
325+ }
326+ }
327+ }
213328
214329private const val UNDECIDED = 0
215330private const val SUSPENDED = 1
0 commit comments