@@ -9,6 +9,7 @@ import kotlinx.atomicfu.*
9
9
import kotlinx.coroutines.internal.*
10
10
import kotlinx.coroutines.intrinsics.*
11
11
import kotlinx.coroutines.selects.*
12
+ import kotlin.concurrent.Volatile
12
13
import kotlin.contracts.*
13
14
import kotlin.coroutines.*
14
15
import kotlin.coroutines.intrinsics.*
@@ -206,10 +207,124 @@ private class LazyStandaloneCoroutine(
206
207
}
207
208
208
209
// 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 (
210
211
context : CoroutineContext ,
211
212
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
+ }
213
328
214
329
private const val UNDECIDED = 0
215
330
private const val SUSPENDED = 1
0 commit comments