1
1
package kotlinx.coroutines
2
2
3
+ import kotlinx.atomicfu.*
3
4
import kotlinx.coroutines.internal.*
4
- import java.util.concurrent.*
5
5
import kotlin.coroutines.*
6
+ import kotlin.time.Duration
6
7
7
8
private val defaultMainDelayOptIn = systemProp(" kotlinx.coroutines.main.delay" , false )
8
9
@@ -21,78 +22,38 @@ private fun initializeDefaultDelay(): Delay {
21
22
return if (main.isMissing() || main !is Delay ) DefaultDelayImpl else main
22
23
}
23
24
24
- internal object DefaultExecutor {
25
- fun shutdown () = DefaultDelayImpl .shutdown()
26
-
27
- fun ensureStarted () = DefaultDelayImpl .ensureStarted()
28
-
29
- fun shutdownForTests (timeout : Long ) = DefaultDelayImpl .shutdownForTests(timeout)
30
-
31
- val isThreadPresent: Boolean get() = DefaultDelayImpl .isThreadPresent
25
+ /* *
26
+ * This method can be invoked after all coroutines are completed to wait for the default delay executor to shut down
27
+ * in response to the lack of tasks.
28
+ *
29
+ * This is only useful in tests to ensure that setting a fresh virtual time source will not confuse the default delay
30
+ * still running the previous test.
31
+ *
32
+ * Does nothing if the default delay executor is not in use.
33
+ *
34
+ * @throws IllegalStateException if the shutdown process notices new tasks entering the system
35
+ * @throws IllegalStateException if the shutdown process times out
36
+ */
37
+ internal fun ensureDefaultDelayDeinitialized (timeout : Duration ) {
38
+ (DefaultDelay as ? DefaultDelayImpl )?.shutdownForTests(timeout)
32
39
}
33
40
34
41
private object DefaultDelayImpl : EventLoopImplBase(), Runnable {
35
- const val THREAD_NAME = " kotlinx.coroutines.DefaultExecutor "
42
+ const val THREAD_NAME = " kotlinx.coroutines.DefaultDelay "
36
43
37
44
init {
38
45
incrementUseCount() // this event loop is never completed
39
46
}
40
47
41
- private const val DEFAULT_KEEP_ALIVE_MS = 1000L // in milliseconds
42
-
43
- private val KEEP_ALIVE_NANOS = TimeUnit .MILLISECONDS .toNanos(
44
- try {
45
- java.lang.Long .getLong(" kotlinx.coroutines.DefaultExecutor.keepAlive" , DEFAULT_KEEP_ALIVE_MS )
46
- } catch (e: SecurityException ) {
47
- DEFAULT_KEEP_ALIVE_MS
48
- })
49
-
50
- @Suppress(" ObjectPropertyName" )
51
- @Volatile
52
- private var _thread : Thread ? = null
53
-
54
- override val thread: Thread
55
- get() = _thread ? : createThreadSync()
56
-
57
- private const val FRESH = 0
58
- private const val ACTIVE = 1
59
- private const val SHUTDOWN_REQ = 2
60
- private const val SHUTDOWN_ACK = 3
61
- private const val SHUTDOWN = 4
62
-
63
- @Volatile
64
- private var debugStatus: Int = FRESH
65
-
66
- private val isShutDown: Boolean get() = debugStatus == SHUTDOWN
67
-
68
- private val isShutdownRequested: Boolean get() {
69
- val debugStatus = debugStatus
70
- return debugStatus == SHUTDOWN_REQ || debugStatus == SHUTDOWN_ACK
71
- }
72
-
73
- override fun enqueue (task : Runnable ) {
74
- if (isShutDown) shutdownError()
75
- super .enqueue(task)
76
- }
77
-
78
- override fun reschedule (now : Long , delayedTask : DelayedTask ) {
79
- // Reschedule on default executor can only be invoked after Dispatchers.shutdown
80
- shutdownError()
81
- }
48
+ private val _thread = atomic<Thread ?>(null )
82
49
83
- private fun shutdownError () {
84
- throw RejectedExecutionException (" DefaultExecutor was shut down. " +
85
- " This error indicates that Dispatchers.shutdown() was invoked prior to completion of exiting coroutines, leaving coroutines in incomplete state. " +
86
- " Please refer to Dispatchers.shutdown documentation for more details" )
87
- }
88
-
89
- override fun shutdown () {
90
- debugStatus = SHUTDOWN
91
- super .shutdown()
50
+ /* * Can only happen when tests close the default executor */
51
+ override fun reschedule (now : Long , delayedTask : DelayedTask ) {
52
+ throw IllegalStateException (" Attempted to schedule $delayedTask at $now after shutdown" )
92
53
}
93
54
94
55
/* *
95
- * All event loops are using DefaultExecutor #invokeOnTimeout to avoid livelock on
56
+ * All event loops are using DefaultDelay #invokeOnTimeout to avoid livelock on
96
57
* ```
97
58
* runBlocking(eventLoop) { withTimeout { while(isActive) { ... } } }
98
59
* ```
@@ -104,100 +65,70 @@ private object DefaultDelayImpl : EventLoopImplBase(), Runnable {
104
65
scheduleInvokeOnTimeout(timeMillis, block)
105
66
106
67
override fun run () {
107
- ThreadLocalEventLoop .setEventLoop(this )
108
- registerTimeLoopThread()
68
+ val currentThread = Thread .currentThread()
69
+ if (! _thread .compareAndSet(null , currentThread)) return // some other thread won the race to start the thread
70
+ val oldName = currentThread.name
71
+ currentThread.name = THREAD_NAME
109
72
try {
110
- var shutdownNanos = Long .MAX_VALUE
111
- if (! notifyStartup()) return
112
- while (true ) {
113
- Thread .interrupted() // just reset interruption flag
114
- var parkNanos = processNextEvent()
115
- if (parkNanos == Long .MAX_VALUE ) {
116
- // nothing to do, initialize shutdown timeout
117
- val now = nanoTime()
118
- if (shutdownNanos == Long .MAX_VALUE ) shutdownNanos = now + KEEP_ALIVE_NANOS
119
- val tillShutdown = shutdownNanos - now
120
- if (tillShutdown <= 0 ) return // shut thread down
121
- parkNanos = parkNanos.coerceAtMost(tillShutdown)
122
- } else
123
- shutdownNanos = Long .MAX_VALUE
124
- if (parkNanos > 0 ) {
125
- // check if shutdown was requested and bail out in this case
126
- if (isShutdownRequested) return
127
- parkNanos(this , parkNanos)
73
+ ThreadLocalEventLoop .setEventLoop(DefaultDelayImpl )
74
+ registerTimeLoopThread()
75
+ try {
76
+ while (true ) {
77
+ Thread .interrupted() // just reset interruption flag
78
+ val parkNanos = processNextEvent()
79
+ if (parkNanos == Long .MAX_VALUE ) break // no more events
80
+ parkNanos(DefaultDelayImpl , parkNanos)
81
+ }
82
+ } finally {
83
+ _thread .value = null
84
+ unregisterTimeLoopThread()
85
+ // recheck if queues are empty after _thread reference was set to null (!!!)
86
+ if (isEmpty) {
87
+ notifyAboutThreadExiting()
88
+ } else {
89
+ /* recreate the thread, as there is still work to do,
90
+ and `unpark` could have awoken the thread we're currently running on */
91
+ startThreadOrObtainSleepingThread()
128
92
}
129
93
}
130
94
} finally {
131
- _thread = null // this thread is dead
132
- acknowledgeShutdownIfNeeded()
133
- unregisterTimeLoopThread()
134
- // recheck if queues are empty after _thread reference was set to null (!!!)
135
- if (! isEmpty) thread // recreate thread if it is needed
95
+ currentThread.name = oldName
136
96
}
137
97
}
138
98
139
- @Synchronized
140
- private fun createThreadSync (): Thread {
141
- return _thread ? : Thread (this , THREAD_NAME ).apply {
142
- _thread = this
143
- /*
144
- * `DefaultExecutor` is a global singleton that creates its thread lazily.
145
- * To isolate the classloaders properly, we are inherting the context classloader from
146
- * the singleton itself instead of using parent' thread one
147
- * in order not to accidentally capture temporary application classloader.
148
- */
149
- contextClassLoader = this @DefaultDelayImpl.javaClass.classLoader
150
- isDaemon = true
151
- start()
152
- }
153
- }
154
-
155
- // used for tests
156
- @Synchronized
157
- internal fun ensureStarted () {
158
- assert { _thread == null } // ensure we are at a clean state
159
- assert { debugStatus == FRESH || debugStatus == SHUTDOWN_ACK }
160
- debugStatus = FRESH
161
- createThreadSync() // create fresh thread
162
- while (debugStatus == FRESH ) (this as Object ).wait()
99
+ override fun startThreadOrObtainSleepingThread (): Thread ? {
100
+ // Check if the thread is already running
101
+ _thread .value?.let { return it }
102
+ /* Now we know that at the moment of this call the thread was not initially running.
103
+ This means that whatever thread is going to be running by the end of this function,
104
+ it's going to notice the tasks it's supposed to run.
105
+ We can return `null` unconditionally. */
106
+ ioView.dispatch(ioView, this )
107
+ return null
163
108
}
164
109
165
- @Synchronized
166
- private fun notifyStartup (): Boolean {
167
- if (isShutdownRequested) return false
168
- debugStatus = ACTIVE
169
- (this as Object ).notifyAll()
170
- return true
171
- }
172
-
173
- @Synchronized // used _only_ for tests
174
- fun shutdownForTests (timeout : Long ) {
175
- val deadline = System .currentTimeMillis() + timeout
176
- if (! isShutdownRequested) debugStatus = SHUTDOWN_REQ
177
- // loop while there is anything to do immediately or deadline passes
178
- while (debugStatus != SHUTDOWN_ACK && _thread != null ) {
179
- _thread ?.let { unpark(it) } // wake up thread if present
180
- val remaining = deadline - System .currentTimeMillis()
181
- if (remaining <= 0 ) break
182
- (this as Object ).wait(timeout)
110
+ fun shutdownForTests (timeout : Duration ) {
111
+ if (_thread .value != null ) {
112
+ val end = System .currentTimeMillis() + timeout.inWholeMilliseconds
113
+ while (true ) {
114
+ check(isEmpty) { " There are tasks in the DefaultExecutor" }
115
+ synchronized(this ) {
116
+ unpark(_thread .value ? : return )
117
+ val toWait = end - System .currentTimeMillis()
118
+ check(toWait > 0 ) { " Timeout waiting for DefaultExecutor to shutdown" }
119
+ (this as Object ).wait(toWait)
120
+ }
121
+ }
183
122
}
184
- // restore fresh status
185
- debugStatus = FRESH
186
123
}
187
124
188
- @Synchronized
189
- private fun acknowledgeShutdownIfNeeded () {
190
- if (! isShutdownRequested) return
191
- debugStatus = SHUTDOWN_ACK
192
- resetAll() // clear queues
193
- (this as Object ).notifyAll()
125
+ private fun notifyAboutThreadExiting () {
126
+ synchronized(this ) { (this as Object ).notifyAll() }
194
127
}
195
128
196
- // User only for testing and nothing else
197
- internal val isThreadPresent
198
- get() = _thread != null
199
-
200
- override fun toString (): String {
201
- return " DefaultExecutor"
202
- }
129
+ override fun toString (): String = " DefaultDelay"
203
130
}
131
+
132
+ /* * A view separate from [Dispatchers.IO].
133
+ * [Int.MAX_VALUE] instead of `1` to avoid needlessly using the [LimitedDispatcher] machinery. */
134
+ private val ioView = Dispatchers .IO .limitedParallelism(Int .MAX_VALUE )
0 commit comments