Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce CoroutineDispatcher.limitedParallelism
- Loading branch information
Showing
9 changed files
with
228 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
82 changes: 82 additions & 0 deletions
82
kotlinx-coroutines-core/common/src/internal/LimitedDispatcher.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
/* | ||
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package kotlinx.coroutines.internal | ||
|
||
import kotlinx.coroutines.* | ||
import kotlin.coroutines.* | ||
import kotlin.jvm.* | ||
|
||
/** | ||
* The result of .limitedParallelism(x) call, dispatcher | ||
* that wraps the given dispatcher, but limits the parallelism level, while | ||
* trying to emulate fairness. | ||
*/ | ||
internal class LimitedDispatcher( | ||
private val dispatcher: CoroutineDispatcher, | ||
private val parallelism: Int | ||
) : CoroutineDispatcher(), Runnable, Delay by (dispatcher as? Delay ?: DefaultDelay) { | ||
|
||
@Volatile | ||
private var runningWorkers = 0 | ||
|
||
private val queue = LockFreeTaskQueue<Runnable>(singleConsumer = false) | ||
|
||
@InternalCoroutinesApi | ||
override fun dispatchYield(context: CoroutineContext, block: Runnable) { | ||
dispatcher.dispatchYield(context, block) | ||
} | ||
|
||
override fun run() { | ||
var fairnessCounter = 0 | ||
while (true) { | ||
val task = queue.removeFirstOrNull() | ||
if (task != null) { | ||
task.run() | ||
// 16 is our out-of-thin-air constant to emulate fairness. Used in JS dispatchers as well | ||
if (++fairnessCounter >= 16 && dispatcher.isDispatchNeeded(EmptyCoroutineContext)) { | ||
// Do "yield" to let other views to execute their runnable as well | ||
// Note that we do not decrement 'runningWorkers' as we still committed to do our part of work | ||
dispatcher.dispatch(EmptyCoroutineContext, this) | ||
return | ||
} | ||
continue | ||
} | ||
|
||
@Suppress("CAST_NEVER_SUCCEEDS") | ||
synchronized(this as SynchronizedObject) { | ||
--runningWorkers | ||
if (queue.size == 0) return | ||
++runningWorkers | ||
fairnessCounter = 0 | ||
} | ||
} | ||
} | ||
|
||
override fun dispatch(context: CoroutineContext, block: Runnable) { | ||
// Add task to queue so running workers will be able to see that | ||
queue.addLast(block) | ||
if (runningWorkers >= parallelism) { | ||
return | ||
} | ||
|
||
/* | ||
* Protect against race when the worker is finished | ||
* right after our check | ||
*/ | ||
@Suppress("CAST_NEVER_SUCCEEDS") | ||
synchronized(this as SynchronizedObject) { | ||
if (runningWorkers >= parallelism) return | ||
++runningWorkers | ||
} | ||
if (dispatcher.isDispatchNeeded(EmptyCoroutineContext)) { | ||
dispatcher.dispatch(EmptyCoroutineContext, this) | ||
} else { | ||
run() | ||
} | ||
} | ||
} | ||
|
||
// Save a few bytecode ops | ||
internal fun Int.checkParallelism() = require(this >= 1) { "Expected positive parallelism level, but got $this" } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
kotlinx-coroutines-core/jvm/test/LimitedParallelismStressTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
/* | ||
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package kotlinx.coroutines | ||
|
||
import org.junit.* | ||
import org.junit.Test | ||
import org.junit.runner.* | ||
import org.junit.runners.* | ||
import java.util.concurrent.atomic.* | ||
import kotlin.test.* | ||
|
||
@RunWith(Parameterized::class) | ||
class LimitedParallelismStressTest(private val targetParallelism: Int) : TestBase() { | ||
|
||
companion object { | ||
@Parameterized.Parameters(name = "{0}") | ||
@JvmStatic | ||
fun params(): Collection<Array<Any>> = listOf(1, 2, 3, 4).map { arrayOf(it) } | ||
} | ||
|
||
@get:Rule | ||
val executor = ExecutorRule(targetParallelism * 2) | ||
private val iterations = 100_000 * stressTestMultiplier | ||
|
||
private val parallelism = AtomicInteger(0) | ||
|
||
private fun checkParallelism() { | ||
val value = parallelism.incrementAndGet() | ||
assertTrue { value <= targetParallelism } | ||
parallelism.decrementAndGet() | ||
} | ||
|
||
@Test | ||
fun testLimited() = runTest { | ||
val view = executor.limitedParallelism(targetParallelism) | ||
repeat(iterations) { | ||
launch(view) { | ||
checkParallelism() | ||
} | ||
} | ||
} | ||
|
||
@Test | ||
fun testUnconfined() = runTest { | ||
val view = Dispatchers.Unconfined.limitedParallelism(targetParallelism) | ||
repeat(iterations) { | ||
launch(executor) { | ||
withContext(view) { | ||
checkParallelism() | ||
} | ||
} | ||
} | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
kotlinx-coroutines-core/jvm/test/LimitedParallelismTest.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,33 @@ | ||
/* | ||
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license. | ||
*/ | ||
|
||
package kotlinx.coroutines | ||
|
||
import org.junit.* | ||
|
||
class LimitedParallelismTest : TestBase() { | ||
|
||
@Test | ||
fun testParallelismSpec() { | ||
assertFailsWith<IllegalArgumentException> { Dispatchers.Default.limitedParallelism(0) } | ||
assertFailsWith<IllegalArgumentException> { Dispatchers.Default.limitedParallelism(-1) } | ||
assertFailsWith<IllegalArgumentException> { Dispatchers.Default.limitedParallelism(Int.MIN_VALUE) } | ||
Dispatchers.Default.limitedParallelism(Int.MAX_VALUE) | ||
} | ||
|
||
@Test | ||
fun testTaskFairness() = runTest { | ||
val executor = newSingleThreadContext("test") | ||
val view = executor.limitedParallelism(1) | ||
val view2 = executor.limitedParallelism(1) | ||
val j1 = launch(view) { | ||
while (true) { | ||
yield() | ||
} | ||
} | ||
val j2 = launch(view2) { j1.cancel() } | ||
joinAll(j1, j2) | ||
executor.close() | ||
} | ||
} |