1
1
package kotlinx.coroutines.experimental.scheduling
2
2
3
+ import kotlinx.atomicfu.atomic
3
4
import kotlinx.coroutines.experimental.Runnable
4
5
import java.io.Closeable
5
6
import java.util.*
6
7
import java.util.concurrent.ConcurrentLinkedQueue
7
8
import java.util.concurrent.Executor
9
+ import java.util.concurrent.TimeUnit
8
10
import java.util.concurrent.locks.LockSupport
9
11
10
12
/* *
@@ -13,9 +15,27 @@ import java.util.concurrent.locks.LockSupport
13
15
class CoroutineScheduler (private val corePoolSize : Int ) : Executor, Closeable {
14
16
15
17
private val workers: Array <PoolWorker >
16
- private val globalWorkQueue: Queue <Task > = ConcurrentLinkedQueue <Task >()
18
+ private val globalWorkQueue = ConcurrentLinkedQueue <Task >()
19
+ private val parkedWorkers = atomic(0 )
20
+ private val random = Random ()
21
+
17
22
@Volatile
18
- private var isClosed = false
23
+ private var isTerminated = false
24
+
25
+ companion object {
26
+ private const val STEAL_ATTEMPTS = 4
27
+ private const val MAX_SPINS = 1000L
28
+ private const val MAX_YIELDS = 500L
29
+ @JvmStatic
30
+ private val MAX_PARK_TIME_NS = TimeUnit .SECONDS .toNanos(1 )
31
+ @JvmStatic
32
+ private val MIN_PARK_TIME_NS = (WORK_STEALING_TIME_RESOLUTION_NS / 4 ).coerceAtLeast(10 ).coerceAtMost(MAX_PARK_TIME_NS )
33
+
34
+ // Local queue 'offer' results
35
+ private const val ADDED = - 1
36
+ private const val ADDED_WITH_OFFLOADING = 0 // Added to the local queue, but pool requires additional worker to keep up
37
+ private const val NOT_ADDED = 1
38
+ }
19
39
20
40
init {
21
41
require(corePoolSize >= 1 , { " Expected positive core pool size, but was $corePoolSize " })
@@ -26,41 +46,111 @@ class CoroutineScheduler(private val corePoolSize: Int) : Executor, Closeable {
26
46
override fun execute (command : Runnable ) = dispatch(command)
27
47
28
48
override fun close () {
29
- isClosed = true
49
+ isTerminated = true
30
50
}
31
51
32
- fun dispatch (command : Runnable , intensive : Boolean = false) {
33
- val task = TimedTask (System .nanoTime(), command)
34
- if (! submitToLocalQueue(task, intensive)) {
52
+ fun dispatch (command : Runnable ) {
53
+ val task = TimedTask (schedulerTimeSource.nanoTime(), command)
54
+
55
+ val offerResult = submitToLocalQueue(task)
56
+ if (offerResult == ADDED ) {
57
+ return
58
+ }
59
+
60
+ if (offerResult == NOT_ADDED ) {
35
61
globalWorkQueue.add(task)
36
62
}
63
+
64
+ unparkIdleWorker()
37
65
}
38
66
39
- private fun submitToLocalQueue (task : Task , intensive : Boolean ): Boolean {
40
- val worker = Thread .currentThread() as ? PoolWorker ? : return false
41
- if (intensive && worker.localQueue.bufferSize > FORKED_TASK_OFFLOAD_THRESHOLD ) return false
42
- worker.localQueue.offer(task, globalWorkQueue)
43
- return true
67
+ private fun unparkIdleWorker () {
68
+ // If no threads are parked don't try to wake anyone
69
+ val parked = parkedWorkers.value
70
+ if (parked == 0 ) {
71
+ return
72
+ }
73
+
74
+ // Try to wake one worker
75
+ repeat(STEAL_ATTEMPTS ) {
76
+ val victim = workers[random.nextInt(workers.size)]
77
+ if (victim.isParking) {
78
+ /*
79
+ * Benign data race, victim can wake up after this check, but before 'unpark' call succeeds,
80
+ * making first 'park' in next idle period a no-op
81
+ */
82
+ LockSupport .unpark(victim)
83
+ return
84
+ }
85
+ }
86
+ }
87
+
88
+
89
+ private fun submitToLocalQueue (task : Task ): Int {
90
+ val worker = Thread .currentThread() as ? PoolWorker ? : return NOT_ADDED
91
+ if (worker.localQueue.offer(task, globalWorkQueue)) {
92
+ // We're close to queue capacity, wakeup anyone to steal
93
+ if (worker.localQueue.bufferSize > QUEUE_SIZE_OFFLOAD_THRESHOLD ) {
94
+ return ADDED_WITH_OFFLOADING
95
+ }
96
+
97
+ return ADDED
98
+ }
99
+
100
+ return ADDED_WITH_OFFLOADING
101
+ }
102
+
103
+ /* *
104
+ * Returns a string identifying state of this scheduler for nicer debugging
105
+ */
106
+ override fun toString (): String {
107
+ var parkedWorkers = 0
108
+ val queueSizes = arrayListOf<Int >()
109
+ for (worker in workers) {
110
+ if (worker.isParking) {
111
+ ++ parkedWorkers
112
+ } else {
113
+ queueSizes + = worker.localQueue.bufferSize
114
+ }
115
+ }
116
+
117
+ return " ${super .toString()} [core pool size = ${workers.size} , " +
118
+ " parked workers = $parkedWorkers , " +
119
+ " active workers buffer sizes = $queueSizes , " +
120
+ " global queue size = ${globalWorkQueue.size} ]"
44
121
}
45
122
46
- private inner class PoolWorker (index : Int ) : Thread(" CoroutinesScheduler-worker-$index " ) {
123
+
124
+ internal inner class PoolWorker (index : Int ) : Thread(" CoroutinesScheduler-worker-$index " ) {
47
125
init {
48
126
isDaemon = true
49
127
}
50
128
51
129
val localQueue: WorkQueue = WorkQueue ()
130
+ /* *
131
+ * Time of last call to [unparkIdleWorker] due to missing tasks deadlines.
132
+ * Used as throttling mechanism to avoid unparking multiple threads when it's not really necessary.
133
+ */
134
+ private var lastExhaustionTime = 0L
52
135
53
136
@Volatile
54
- var yields = 0
137
+ var isParking = false
138
+ @Volatile
139
+ private var spins = 0L
140
+ private var yields = 0L
141
+ private var parkTimeNs = MIN_PARK_TIME_NS
142
+ private var rngState = random.nextInt()
55
143
56
144
override fun run () {
57
- while (! isClosed ) {
145
+ while (! isTerminated ) {
58
146
try {
59
147
val job = findTask()
60
148
if (job == null ) {
61
- awaitWork()
149
+ // Wait for a job with potential park
150
+ idle()
62
151
} else {
63
- yields = 0
152
+ idleReset()
153
+ checkExhaustion(job)
64
154
job.task.run ()
65
155
}
66
156
} catch (e: Throwable ) {
@@ -69,15 +159,77 @@ class CoroutineScheduler(private val corePoolSize: Int) : Executor, Closeable {
69
159
}
70
160
}
71
161
72
- private fun awaitWork () {
73
- // Temporary solution
74
- if (++ yields > 100000 ) {
75
- LockSupport .parkNanos(WORK_STEALING_TIME_RESOLUTION / 2 )
162
+ private fun checkExhaustion (job : Task ) {
163
+ val parked = parkedWorkers.value
164
+ if (parked == 0 ) {
165
+ return
166
+ }
167
+
168
+ // Check last exhaustion time to avoid the race between steal and next task execution
169
+ val now = schedulerTimeSource.nanoTime()
170
+ if (now - job.submissionTime >= WORK_STEALING_TIME_RESOLUTION_NS && now - lastExhaustionTime >= WORK_STEALING_TIME_RESOLUTION_NS * 5 ) {
171
+ lastExhaustionTime = now
172
+ unparkIdleWorker()
173
+ }
174
+ }
175
+
176
+ /*
177
+ * Marsaglia xorshift RNG with period 2^32-1 for work stealing purposes.
178
+ * ThreadLocalRandom cannot be used to support Android and ThreadLocal<Random> is up to 15% slower on ktor benchmarks
179
+ */
180
+ internal fun nextInt (upperBound : Int ): Int {
181
+ rngState = rngState xor (rngState shl 13 )
182
+ rngState = rngState xor (rngState shr 17 )
183
+ rngState = rngState xor (rngState shl 5 )
184
+ val mask = upperBound - 1
185
+ // Fast path for power of two bound
186
+ if (mask and upperBound == 0 ) {
187
+ return rngState and mask
76
188
}
189
+
190
+ return (rngState and Int .MAX_VALUE ) % upperBound
191
+ }
192
+
193
+ private fun idle () {
194
+ /*
195
+ * Simple adaptive await of work:
196
+ * Spin on the volatile field with an empty loop in hope that new work will arrive,
197
+ * then start yielding to reduce CPU pressure, and finally start adaptive parking.
198
+ *
199
+ * The main idea is not to park while it's possible (otherwise throughput on asymmetric workloads suffers due to too frequent
200
+ * park/unpark calls and delays between job submission and thread queue checking)
201
+ */
202
+ when {
203
+ spins < MAX_SPINS -> ++ spins
204
+ ++ yields <= MAX_YIELDS -> Thread .yield ()
205
+ else -> {
206
+ if (! isParking) {
207
+ isParking = true
208
+ parkedWorkers.incrementAndGet()
209
+ }
210
+
211
+ if (parkTimeNs < MAX_PARK_TIME_NS ) {
212
+ parkTimeNs = (parkTimeNs * 1.5 ).toLong().coerceAtMost(MAX_PARK_TIME_NS )
213
+ }
214
+
215
+ LockSupport .parkNanos(parkTimeNs)
216
+ }
217
+ }
218
+ }
219
+
220
+ private fun idleReset () {
221
+ if (isParking) {
222
+ isParking = false
223
+ parkTimeNs = MIN_PARK_TIME_NS
224
+ parkedWorkers.decrementAndGet()
225
+ }
226
+
227
+ spins = 0
228
+ yields = 0
77
229
}
78
230
79
231
private fun findTask (): Task ? {
80
- // TODO explain, probabilistic check with park counter ?
232
+ // TODO probabilistic check if thread is not idle ?
81
233
var task: Task ? = globalWorkQueue.poll()
82
234
if (task != null ) return task
83
235
@@ -92,16 +244,17 @@ class CoroutineScheduler(private val corePoolSize: Int) : Executor, Closeable {
92
244
return null
93
245
}
94
246
95
- while (true ) {
96
- val worker = workers[RANDOM_PROVIDER ().nextInt(workers.size)]
247
+ // Probe a couple of workers
248
+ repeat(STEAL_ATTEMPTS ) {
249
+ val worker = workers[nextInt(workers.size)]
97
250
if (worker != = this ) {
98
- worker. localQueue.offloadWork( true ) {
99
- localQueue.offer(it, globalWorkQueue)
251
+ if ( localQueue.trySteal(worker.localQueue, globalWorkQueue) ) {
252
+ return @repeat
100
253
}
101
-
102
- return localQueue.poll()
103
254
}
104
255
}
256
+
257
+ return localQueue.poll()
105
258
}
106
259
}
107
260
}
0 commit comments