Skip to content

Commit d06effe

Browse files
committed
Switch Worker into a Blocking mode if it tries to run runBlocking with a CPU permit
And reacquire CPU permit after runBlocking finishes. This should resolve Dispatchers.Default starvation in cases where runBlocking is used to run suspend functions from non-suspend execution context. Kotlin#3983 / IJPL-721
1 parent 8c516f5 commit d06effe

File tree

4 files changed

+80
-1
lines changed

4 files changed

+80
-1
lines changed

kotlinx-coroutines-core/jvm/src/Builders.kt

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
package kotlinx.coroutines
66

7+
import kotlinx.coroutines.scheduling.*
8+
import kotlinx.coroutines.scheduling.CoroutineScheduler
79
import java.util.concurrent.locks.*
810
import kotlin.contracts.*
911
import kotlin.coroutines.*
@@ -86,6 +88,7 @@ private class BlockingCoroutine<T>(
8688
@Suppress("UNCHECKED_CAST")
8789
fun joinBlocking(): T {
8890
registerTimeLoopThread()
91+
var cpuPermitReleased: Boolean? = null
8992
try {
9093
eventLoop?.incrementUseCount()
9194
try {
@@ -95,13 +98,20 @@ private class BlockingCoroutine<T>(
9598
val parkNanos = eventLoop?.processNextEvent() ?: Long.MAX_VALUE
9699
// note: process next even may loose unpark flag, so check if completed before parking
97100
if (isCompleted) break
101+
if (parkNanos > 0 && cpuPermitReleased == null) {
102+
val worker = Thread.currentThread() as? CoroutineScheduler.Worker
103+
cpuPermitReleased = worker?.releaseCpu() ?: false
104+
}
98105
parkNanos(this, parkNanos)
99106
}
100107
} finally { // paranoia
101108
eventLoop?.decrementUseCount()
102109
}
103110
} finally { // paranoia
104111
unregisterTimeLoopThread()
112+
if (cpuPermitReleased == true) {
113+
(Thread.currentThread() as CoroutineScheduler.Worker).reacquireCpu()
114+
}
105115
}
106116
// now return result
107117
val state = this.state.unboxState()

kotlinx-coroutines-core/jvm/src/scheduling/CoroutineScheduler.kt

+49
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,55 @@ internal class CoroutineScheduler(
690690
return hadCpu
691691
}
692692

693+
/** only for [runBlocking] */
694+
fun releaseCpu(): Boolean {
695+
assert { state == WorkerState.CPU_ACQUIRED || state == WorkerState.BLOCKING }
696+
return tryReleaseCpu(WorkerState.BLOCKING).also {
697+
if (it) {
698+
incrementBlockingTasks()
699+
}
700+
}
701+
}
702+
703+
/** only for [runBlocking] */
704+
fun reacquireCpu() {
705+
assert { state == WorkerState.BLOCKING }
706+
decrementBlockingTasks()
707+
if (tryAcquireCpuPermit()) return
708+
class CpuPermitTransfer {
709+
private val status = atomic(false)
710+
fun check(): Boolean = status.value
711+
fun complete(): Boolean = status.compareAndSet(false, true)
712+
}
713+
val permitTransfer = CpuPermitTransfer()
714+
val blockedWorker = this@Worker
715+
scheduler.dispatch(Runnable {
716+
// this code runs in a different worker thread that holds a CPU token
717+
val cpuHolder = currentThread() as Worker
718+
assert { cpuHolder.state == WorkerState.CPU_ACQUIRED }
719+
if (permitTransfer.complete()) {
720+
cpuHolder.state = WorkerState.BLOCKING
721+
LockSupport.unpark(blockedWorker)
722+
}
723+
}, taskContext = NonBlockingContext)
724+
state = WorkerState.PARKING
725+
while (true) {
726+
if (permitTransfer.check()) {
727+
state = WorkerState.CPU_ACQUIRED
728+
break
729+
}
730+
if (tryAcquireCpuPermit()) {
731+
if (!permitTransfer.complete()) {
732+
// race: transfer was completed by another thread
733+
releaseCpuPermit()
734+
}
735+
assert { state == WorkerState.CPU_ACQUIRED }
736+
break
737+
}
738+
LockSupport.parkNanos(RUN_BLOCKING_CPU_REACQUIRE_PARK_NS)
739+
}
740+
}
741+
693742
override fun run() = runWorker()
694743

695744
@JvmField

kotlinx-coroutines-core/jvm/src/scheduling/Tasks.kt

+5
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,11 @@ internal val WORK_STEALING_TIME_RESOLUTION_NS = systemProp(
1919
"kotlinx.coroutines.scheduler.resolution.ns", 100000L
2020
)
2121

22+
@JvmField
23+
internal val RUN_BLOCKING_CPU_REACQUIRE_PARK_NS = systemProp(
24+
"kotlinx.coroutines.scheduler.runBlocking.cpu.reacquire.ns", 250L * 1000 * 1000
25+
)
26+
2227
/**
2328
* The maximum number of threads allocated for CPU-bound tasks at the default set of dispatchers.
2429
*

kotlinx-coroutines-core/jvm/test/scheduling/BlockingCoroutineDispatcherTest.kt

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package kotlinx.coroutines.scheduling
22

3-
import kotlinx.coroutines.testing.*
43
import kotlinx.coroutines.*
54
import org.junit.*
65
import org.junit.rules.*
@@ -171,4 +170,20 @@ class BlockingCoroutineDispatcherTest : SchedulerTestBase() {
171170
fun testZeroParallelism() {
172171
blockingDispatcher(0)
173172
}
173+
174+
@Test
175+
fun testNoCpuStarvationWithDeepRunBlocking() {
176+
val maxDepth = CORES_COUNT * 3 + 3
177+
fun body(depth: Int) {
178+
if (depth == maxDepth) return
179+
runBlocking(dispatcher) {
180+
launch(dispatcher) {
181+
body(depth + 1)
182+
}
183+
}
184+
}
185+
186+
body(1)
187+
checkPoolThreadsCreated(maxDepth..maxDepth + 1)
188+
}
174189
}

0 commit comments

Comments
 (0)