@@ -2,7 +2,6 @@ package kotlinx.coroutines.experimental.scheduling
2
2
3
3
import kotlinx.atomicfu.*
4
4
import kotlinx.coroutines.experimental.*
5
- import netscape.security.Privilege.*
6
5
import java.io.*
7
6
import java.util.*
8
7
import java.util.concurrent.*
@@ -52,7 +51,7 @@ internal class CoroutineScheduler(
52
51
private val maxPoolSize : Int = corePoolSize * 128
53
52
) : Closeable {
54
53
55
- private val globalWorkQueue: GlobalQueue = ConcurrentLinkedQueue < Task > ()
54
+ private val globalWorkQueue: LockFreeQueue = LockFreeQueue ()
56
55
57
56
/*
58
57
* Permits to execute non-blocking (~CPU-intensive) tasks.
@@ -62,8 +61,41 @@ internal class CoroutineScheduler(
62
61
*/
63
62
private val cpuPermits = Semaphore (corePoolSize, false )
64
63
65
- // TODO: Make queue intrusive with PoolWorker and [probably] replace with stack
66
- private val parkedWorkers = ConcurrentLinkedQueue <PoolWorker >()
64
+ /*
65
+ * The stack of parker workers (with an artificial object to make call-sites more understandable).
66
+ * Every worker registers itself in a stack before parking (if it was not previously registered)
67
+ * and callers of [requestCpuWorker] will try to unpark a thread from the top of a stack.
68
+ * This is a form of intrusive garbage-free Treiber stack where PoolWorker also is a stack node.
69
+ *
70
+ * The stack is better than a queue (even with contention on top) because it unparks threads
71
+ * in most-recently used order, improving both performance and locality.
72
+ * Moreover, it decreases threads thrashing, if the pool has n threads when only n / 2 is required,
73
+ * the latter half will never be unparked and will terminate itself after [BLOCKING_WORKER_KEEP_ALIVE_NS].
74
+ */
75
+ @Suppress(" ClassName" )
76
+ private object parkedWorkersStack
77
+ private val head = atomic<PoolWorker ?>(null )
78
+
79
+ @Suppress(" unused" )
80
+ private fun parkedWorkersStack.push (next : PoolWorker ) {
81
+ head.loop { h ->
82
+ next.nextParkedWorker = h
83
+ if (head.compareAndSet(h, next)) return
84
+ }
85
+ }
86
+
87
+ @Suppress(" unused" )
88
+ private fun parkedWorkersStack.pop (): PoolWorker ? {
89
+ // TODO investigate ABA possibility
90
+ head.loop { h ->
91
+ if (h == null ) return null
92
+ val next = h.nextParkedWorker
93
+ if (head.compareAndSet(h, next)) {
94
+ h.nextParkedWorker = null
95
+ return h
96
+ }
97
+ }
98
+ }
67
99
68
100
/* *
69
101
* State of worker threads
@@ -73,22 +105,21 @@ internal class CoroutineScheduler(
73
105
*/
74
106
private val workers: Array <PoolWorker ?>
75
107
76
- // TODO describe, asserts?
108
+ /*
109
+ * Long describing state of workers in this pool.
110
+ * Currently includes created and blocking workers
111
+ */
77
112
private val controlState = atomic(0L )
78
113
private val createdWorkers: Int inline get() = (controlState.value and CREATED_MASK ).toInt()
79
114
private val blockingWorkers: Int inline get() = (controlState.value and BLOCKING_MASK shr 21 ).toInt()
80
- private val idleWorkers: Int inline get() = (controlState.value and IDLE_MASK shr 42 ).toInt()
81
115
82
116
private inline fun createdWorkers (state : Long ): Int = (state and CREATED_MASK ).toInt()
83
117
private inline fun blockingWorkers (state : Long ): Int = (state and BLOCKING_MASK shr 21 ).toInt()
84
- private inline fun idleWorkers (state : Long ): Int = (state and IDLE_MASK shr 42 ).toInt()
85
118
86
119
private inline fun incrementCreatedWorkers (): Int = createdWorkers(controlState.addAndGet(1L ))
87
120
private inline fun decrementCreatedWorkers (): Int = createdWorkers(controlState.addAndGet(- 1L ))
88
121
private inline fun incrementBlockingWorkers (): Int = blockingWorkers(controlState.addAndGet(1L shl 21 ))
89
122
private inline fun decrementBlockingWorkers (): Int = blockingWorkers(controlState.addAndGet(- (1L shl 21 )))
90
- private inline fun incrementIdleWorkers (): Int = idleWorkers(controlState.addAndGet(1L shl 42 ))
91
- private inline fun decrementIdleWorkers (): Int = idleWorkers(controlState.addAndGet(- (1L shl 42 )))
92
123
93
124
private val random = Random ()
94
125
private val isTerminated = atomic(false )
@@ -106,7 +137,7 @@ internal class CoroutineScheduler(
106
137
.coerceAtMost(MAX_PARK_TIME_NS )
107
138
108
139
@JvmStatic
109
- private val BLOCKING_WORKER_KEEP_ALIVE_NS = TimeUnit .SECONDS .toNanos(2 )
140
+ private val BLOCKING_WORKER_KEEP_ALIVE_NS = TimeUnit .SECONDS .toNanos(5 )
110
141
111
142
// Local queue 'add' results
112
143
private const val ADDED = - 1
@@ -119,9 +150,9 @@ internal class CoroutineScheduler(
119
150
private const val ALLOWED = 0
120
151
private const val TERMINATED = 1
121
152
153
+ // Masks of control state
122
154
private const val CREATED_MASK : Long = (1L shl 21 ) - 1
123
155
private const val BLOCKING_MASK : Long = CREATED_MASK shl 21
124
- private const val IDLE_MASK : Long = BLOCKING_MASK shl 21
125
156
}
126
157
127
158
init {
@@ -144,7 +175,7 @@ internal class CoroutineScheduler(
144
175
}
145
176
146
177
/*
147
- * Closes current scheduler and waits until all threads will be stopped.
178
+ * Closes current scheduler and waits until all threads are stopped.
148
179
* This method uses unsafe API (unconditional unparks, ignoring interruptions etc.)
149
180
* and intended to be used only for testing. Invocation has no additional effect if already closed.
150
181
*/
@@ -162,7 +193,7 @@ internal class CoroutineScheduler(
162
193
if (it.isAlive) {
163
194
// Unparking alive thread is unsafe in general, but acceptable for testing purposes
164
195
LockSupport .unpark(it)
165
- it.join(1_000 )
196
+ it.join(100 )
166
197
}
167
198
168
199
++ finished
@@ -201,22 +232,24 @@ internal class CoroutineScheduler(
201
232
private fun requestCpuWorker () {
202
233
// No CPU available -- nothing to request
203
234
if (cpuPermits.availablePermits() == 0 ) {
235
+ tryUnpark()
204
236
return
205
237
}
206
238
207
239
/*
208
- * Fast path -- we have retired or parked worker, unpark it, and we're done.
240
+ * Fast path -- we have retired or parked worker, unpark it and we're done.
209
241
* The data race here: when only one permit is available, multiple retired workers
210
242
* can be unparked, but only one will continue execution, so we're overproviding with threads
211
243
* in case of race to avoid spurious starvation
212
244
*/
213
245
if (tryUnpark()) return
214
246
215
247
/*
248
+ * Create a new thread.
216
249
* It's not preferable to use 'cpuWorkersCounter' here (moreover, it's implicitly here as corePoolSize - cpuPermits.availableTokens),
217
250
* cpuWorkersCounter doesn't take into account threads which are created (and either running or parked), but haven't
218
251
* CPU token: retiring workers, recently unparked workers before `findTask` call, etc.
219
- * So if we will use cpuWorkersCounter, we start to overprovision too much.
252
+ * So if we will use cpuWorkersCounter, we start to overprovide with threads too much.
220
253
*/
221
254
val state = controlState.value
222
255
val created = createdWorkers(state)
@@ -233,7 +266,7 @@ internal class CoroutineScheduler(
233
266
234
267
private fun tryUnpark (): Boolean {
235
268
while (true ) {
236
- val worker = parkedWorkers.poll () ? : return false
269
+ val worker = parkedWorkersStack.pop () ? : return false
237
270
if (! worker.registeredInParkedQueue.value) {
238
271
continue // Someone else succeeded
239
272
} else if (! worker.registeredInParkedQueue.compareAndSet(true , false )) {
@@ -394,14 +427,14 @@ internal class CoroutineScheduler(
394
427
}
395
428
}
396
429
397
- return " ${super .toString()} [core pool size = ${workers.size} , " +
430
+ return " ${super .toString()} [core pool size = $corePoolSize , " +
398
431
" CPU workers = $cpuWorkers , " +
399
432
" blocking workers = $blockingWorkers , " +
400
433
" parked workers = $parkedWorkers , " +
401
434
" retired workers = $retired , " +
402
435
" finished workers = $finished , " +
403
- " running workers queues = $queueSizes , " +
404
- " global queue size = ${globalWorkQueue.size} ]"
436
+ " running workers queues = $queueSizes , " +
437
+ " global queue size = ${globalWorkQueue.size() } ]"
405
438
}
406
439
407
440
// todo: make name of the pool configurable (optional parameter to CoroutineScheduler) and base thread names on it
@@ -436,11 +469,12 @@ internal class CoroutineScheduler(
436
469
val terminationState = atomic(ALLOWED )
437
470
438
471
/* *
439
- * Whether worker was added to [parkedWorkers ].
472
+ * Whether worker was added to [parkedWorkersStack ].
440
473
* Worker registers itself in this queue once and will stay there until
441
474
* someone will call [Queue.poll] which return it, then this flag is reset.
442
475
*/
443
476
val registeredInParkedQueue = atomic(false )
477
+ var nextParkedWorker: PoolWorker ? = null
444
478
445
479
/* *
446
480
* Tries to acquire CPU token if worker doesn't have one
@@ -591,7 +625,7 @@ internal class CoroutineScheduler(
591
625
}
592
626
593
627
if (registeredInParkedQueue.compareAndSet(false , true )) {
594
- parkedWorkers.add (this )
628
+ parkedWorkersStack.push (this )
595
629
}
596
630
597
631
tryReleaseCpu(WorkerState .PARKING )
@@ -603,14 +637,14 @@ internal class CoroutineScheduler(
603
637
private fun blockingWorkerIdle () {
604
638
tryReleaseCpu(WorkerState .PARKING )
605
639
if (registeredInParkedQueue.compareAndSet(false , true )) {
606
- parkedWorkers.add (this )
640
+ parkedWorkersStack.push (this )
607
641
}
608
642
609
643
terminationState.value = ALLOWED
610
644
val time = System .nanoTime()
611
645
LockSupport .parkNanos(BLOCKING_WORKER_KEEP_ALIVE_NS )
612
646
// Protection against spurious wakeups of parkNanos
613
- if (System .nanoTime() - time > BLOCKING_WORKER_KEEP_ALIVE_NS ) {
647
+ if (System .nanoTime() - time >= BLOCKING_WORKER_KEEP_ALIVE_NS ) {
614
648
terminateWorker()
615
649
}
616
650
}
@@ -619,6 +653,14 @@ internal class CoroutineScheduler(
619
653
* Stops execution of current thread and removes it from [createdWorkers]
620
654
*/
621
655
private fun terminateWorker () {
656
+ // Last ditch polling: try to find blocking task before termination
657
+ val task = globalWorkQueue.pollBlockingMode()
658
+ if (task != null ) {
659
+ localQueue.add(task, globalWorkQueue)
660
+ return
661
+ }
662
+
663
+
622
664
synchronized(workers) {
623
665
// Someone else terminated, bail out
624
666
if (createdWorkers <= corePoolSize) {
@@ -692,8 +734,15 @@ internal class CoroutineScheduler(
692
734
return trySteal()
693
735
}
694
736
695
- // Retiring worker, only local queue polling is allowed
696
- return localQueue.poll()
737
+ /*
738
+ * If the local queue is empty, try to extract blocking task from global queue.
739
+ * It's helpful for two reasons:
740
+ * 1) We won't call excess park/unpark here and someone's else CPU token won't be transferred,
741
+ * which is a performance win
742
+ * 2) It helps with rare race when external submitter sends depending blocking tasks
743
+ * one by one and one of the requested workers may miss CPU token
744
+ */
745
+ return localQueue.poll() ? : globalWorkQueue.pollBlockingMode()
697
746
}
698
747
699
748
private fun trySteal (): Task ? {
0 commit comments