1
+ package kotlinx.coroutines.internal
2
+
3
+ import kotlinx.atomicfu.*
4
+ import kotlinx.coroutines.*
5
+ import kotlinx.coroutines.scheduling.ParallelismCompensation
6
+ import kotlin.coroutines.*
7
+
8
+ /* *
9
+ * Introduced as part of IntelliJ patches.
10
+ *
11
+ * CoroutineDispatchers may optionally implement this interface to declare an ability to construct [SoftLimitedDispatcher]
12
+ * on top of themselves. This is not possible in general case, because the worker of the underlying dispatcher must
13
+ * implement [ParallelismCompensation] and properly propagate such requests to the task it is running.
14
+ */
15
+ internal interface SoftLimitedParallelism {
16
+ fun softLimitedParallelism (parallelism : Int , name : String? ): CoroutineDispatcher
17
+ }
18
+
19
+ /* *
20
+ * Introduced as part of IntelliJ patches.
21
+ */
22
+ internal fun CoroutineDispatcher.softLimitedParallelism (parallelism : Int , name : String? ): CoroutineDispatcher {
23
+ if (this is SoftLimitedParallelism ) {
24
+ return this .softLimitedParallelism(parallelism, name)
25
+ }
26
+ // SoftLimitedDispatcher cannot be used on top of LimitedDispatcher, because the latter doesn't propagate compensation requests
27
+ throw UnsupportedOperationException (" CoroutineDispatcher.softLimitedParallelism cannot be applied to $this " )
28
+ }
29
+
30
+ /* *
31
+ * Introduced as part of IntelliJ patches.
32
+ *
33
+ * Shamelessly copy-pasted from [LimitedDispatcher], but [ParallelismCompensation] is
34
+ * implemented for [Worker] to allow compensation.
35
+ *
36
+ * [ParallelismCompensation] breaks the contract of [LimitedDispatcher] so a separate class is made to implement a
37
+ * dispatcher that mostly behaves as limited, but can temporarily increase parallelism if necessary.
38
+ */
39
+ internal class SoftLimitedDispatcher (
40
+ private val dispatcher : CoroutineDispatcher ,
41
+ parallelism : Int ,
42
+ private val name : String?
43
+ ) : CoroutineDispatcher(), Delay by (dispatcher as ? Delay ? : DefaultDelay ), SoftLimitedParallelism {
44
+ private val initialParallelism = parallelism
45
+ // `parallelism limit - runningWorkers`; may be < 0 if decompensation is expected
46
+ private val availablePermits = atomic(parallelism)
47
+
48
+ private val queue = LockFreeTaskQueue <Runnable >(singleConsumer = false )
49
+
50
+ private val workerAllocationLock = SynchronizedObject ()
51
+
52
+ override fun limitedParallelism (parallelism : Int , name : String? ): CoroutineDispatcher {
53
+ return super .limitedParallelism(parallelism, name)
54
+ }
55
+
56
+ override fun softLimitedParallelism (parallelism : Int , name : String? ): CoroutineDispatcher {
57
+ parallelism.checkParallelism()
58
+ if (parallelism >= initialParallelism) return namedOrThis(name)
59
+ return SoftLimitedDispatcher (this , parallelism, name)
60
+ }
61
+
62
+ override fun dispatch (context : CoroutineContext , block : Runnable ) {
63
+ dispatchInternal(block) { worker ->
64
+ dispatcher.safeDispatch(this , worker)
65
+ }
66
+ }
67
+
68
+ @InternalCoroutinesApi
69
+ override fun dispatchYield (context : CoroutineContext , block : Runnable ) {
70
+ dispatchInternal(block) { worker ->
71
+ dispatcher.dispatchYield(this , worker)
72
+ }
73
+ }
74
+
75
+ /* *
76
+ * Tries to dispatch the given [block].
77
+ * If there are not enough workers, it starts a new one via [startWorker].
78
+ */
79
+ private inline fun dispatchInternal (block : Runnable , startWorker : (Worker ) -> Unit ) {
80
+ queue.addLast(block)
81
+ if (availablePermits.value <= 0 ) return
82
+ if (! tryAllocateWorker()) return
83
+ val task = obtainTaskOrDeallocateWorker() ? : return
84
+ startWorker(Worker (task))
85
+ }
86
+
87
+ /* *
88
+ * Tries to obtain the permit to start a new worker.
89
+ */
90
+ private fun tryAllocateWorker (): Boolean {
91
+ synchronized(workerAllocationLock) {
92
+ val permits = availablePermits.value
93
+ if (permits <= 0 ) return false
94
+ return availablePermits.compareAndSet(permits, permits - 1 )
95
+ }
96
+ }
97
+
98
+ /* *
99
+ * Obtains the next task from the queue, or logically deallocates the worker if the queue is empty.
100
+ */
101
+ private fun obtainTaskOrDeallocateWorker (): Runnable ? {
102
+ val permits = availablePermits.value
103
+ if (permits < 0 ) { // decompensation
104
+ if (availablePermits.compareAndSet(permits, permits + 1 )) {
105
+ return null
106
+ }
107
+ }
108
+ while (true ) {
109
+ when (val nextTask = queue.removeFirstOrNull()) {
110
+ null -> synchronized(workerAllocationLock) {
111
+ availablePermits.incrementAndGet()
112
+ if (queue.size == 0 ) return null
113
+ availablePermits.decrementAndGet()
114
+ }
115
+ else -> return nextTask
116
+ }
117
+ }
118
+ }
119
+
120
+ override fun toString () = name ? : " $dispatcher .softLimitedParallelism($initialParallelism )"
121
+
122
+ /* *
123
+ * Every running Worker holds a permit
124
+ */
125
+ private inner class Worker (private var currentTask : Runnable ) : Runnable, ParallelismCompensation {
126
+ override fun run () {
127
+ var fairnessCounter = 0
128
+ while (true ) {
129
+ try {
130
+ currentTask.run ()
131
+ } catch (e: Throwable ) {
132
+ handleCoroutineException(EmptyCoroutineContext , e)
133
+ }
134
+ currentTask = obtainTaskOrDeallocateWorker() ? : return
135
+ // 16 is our out-of-thin-air constant to emulate fairness. Used in JS dispatchers as well
136
+ if (++ fairnessCounter >= 16 && dispatcher.safeIsDispatchNeeded(this @SoftLimitedDispatcher)) {
137
+ // Do "yield" to let other views execute their runnable as well
138
+ // Note that we do not decrement 'runningWorkers' as we are still committed to our part of work
139
+ dispatcher.safeDispatch(this @SoftLimitedDispatcher, this )
140
+ return
141
+ }
142
+ }
143
+ }
144
+
145
+ override fun increaseParallelismAndLimit () {
146
+ val newTask = obtainTaskOrDeallocateWorker() // either increases the number of permits or we launch a new worker (which holds a permit)
147
+ if (newTask != null ) {
148
+ dispatcher.safeDispatch(this @SoftLimitedDispatcher, Worker (newTask))
149
+ }
150
+ (currentTask as ? ParallelismCompensation )?.increaseParallelismAndLimit()
151
+ }
152
+
153
+ override fun decreaseParallelismLimit () {
154
+ try {
155
+ (currentTask as ? ParallelismCompensation )?.decreaseParallelismLimit()
156
+ } finally {
157
+ availablePermits.decrementAndGet()
158
+ }
159
+ }
160
+ }
161
+ }
0 commit comments