@@ -167,9 +167,11 @@ internal actual class UndispatchedCoroutine<in T>actual constructor (
167
167
uCont : Continuation <T >
168
168
) : ScopeCoroutine<T>(if (context[UndispatchedMarker ] == null) context + UndispatchedMarker else context, uCont) {
169
169
170
- /*
171
- * The state is thread-local because this coroutine can be used concurrently.
172
- * Scenario of usage (withContinuationContext):
170
+ /* *
171
+ * The state of [ThreadContextElement]s associated with the current undispatched coroutine.
172
+ * It is stored in a thread local because this coroutine can be used concurrently in suspend-resume race scenario.
173
+ * See the followin, boiled down example with inlined `withContinuationContext` body:
174
+ * ```
173
175
* val state = saveThreadContext(ctx)
174
176
* try {
175
177
* invokeSmthWithThisCoroutineAsCompletion() // Completion implies that 'afterResume' will be called
@@ -178,8 +180,40 @@ internal actual class UndispatchedCoroutine<in T>actual constructor (
178
180
* thisCoroutine().clearThreadContext() // Concurrently the "smth" could've been already resumed on a different thread
179
181
* // and it also calls saveThreadContext and clearThreadContext
180
182
* }
183
+ * ```
184
+ *
185
+ * Usage note:
186
+ *
187
+ * This part of the code is performance-sensitive.
188
+ * It is a well-established pattern to wrap various activities into system-specific undispatched
189
+ * `withContext` for the sake of logging, MDC, tracing etc., meaning that there exists thousands of
190
+ * undispatched coroutines.
191
+ * Each access to Java's [ThreadLocal] leaves a footprint in the corresponding Thread's `ThreadLocalMap`
192
+ * that is cleared automatically as soon as the associated thread-local (-> UndispatchedCoroutine) is garbage collected.
193
+ * When such coroutines are promoted to old generation, `ThreadLocalMap`s become bloated and an arbitrary accesses to thread locals
194
+ * start to consume significant amount of CPU because these maps are open-addressed and cleaned up incrementally on each access.
195
+ * (You can read more about this effect as "GC nepotism").
196
+ *
197
+ * To avoid that, we attempt to narrow down the lifetime of this thread local as much as possible:
198
+ * * It's never accessed when we are sure there are no thread context elements
199
+ * * It's cleaned up via [ThreadLocal.remove] as soon as the coroutine is suspended or finished.
200
+ */
201
+ private val threadStateToRecover = ThreadLocal <Pair <CoroutineContext , Any ?>>()
202
+
203
+ /*
204
+ * Indicates that a coroutine has at least one thread context element associated with it
205
+ * and that 'threadStateToRecover' is going to be set in case of dispatchhing in order to preserve them.
206
+ * Better than nullable thread-local for easier debugging.
207
+ *
208
+ * It is used as a performance optimization to avoid 'threadStateToRecover' initialization
209
+ * (note: tl.get() initializes thread local),
210
+ * and is prone to false-positives as it is never reset: otherwise
211
+ * it may lead to logical data races between suspensions point where
212
+ * coroutine is yet being suspended in one thread while already being resumed
213
+ * in another.
181
214
*/
182
- private var threadStateToRecover = ThreadLocal <Pair <CoroutineContext , Any ?>>()
215
+ @Volatile
216
+ private var threadLocalIsSet = false
183
217
184
218
init {
185
219
/*
@@ -213,19 +247,22 @@ internal actual class UndispatchedCoroutine<in T>actual constructor (
213
247
}
214
248
215
249
fun saveThreadContext (context : CoroutineContext , oldValue : Any? ) {
250
+ threadLocalIsSet = true // Specify that thread-local is touched at all
216
251
threadStateToRecover.set(context to oldValue)
217
252
}
218
253
219
254
fun clearThreadContext (): Boolean {
220
- if ( threadStateToRecover.get() == null ) return false
221
- threadStateToRecover.set( null )
222
- return true
255
+ return ! (threadLocalIsSet && threadStateToRecover.get() == null ). also {
256
+ threadStateToRecover.remove( )
257
+ }
223
258
}
224
259
225
260
override fun afterResume (state : Any? ) {
226
- threadStateToRecover.get()?.let { (ctx, value) ->
227
- restoreThreadContext(ctx, value)
228
- threadStateToRecover.set(null )
261
+ if (threadLocalIsSet) {
262
+ threadStateToRecover.get()?.let { (ctx, value) ->
263
+ restoreThreadContext(ctx, value)
264
+ }
265
+ threadStateToRecover.remove()
229
266
}
230
267
// resume undispatched -- update context but stay on the same dispatcher
231
268
val result = recoverResult(state, uCont)
0 commit comments