Skip to content

Commit c7545b5

Browse files
authored
Fix newSingleThreadContext awaiting cancelled scheduled coroutines (#3769)
* Fix newSingleThreadContext awaiting cancelled scheduled coroutines Before the change, the Worker is not notified about its work being cancelled, due to no API being present for that. We work around the issue by checking every 100 milliseconds whether cancellation happened. Also, alias 'newSingleThreadContext' to 'newFixedThreadPoolContext(1)` for the sake of consistent implementation Fixes #3768
1 parent 897599f commit c7545b5

File tree

4 files changed

+68
-39
lines changed

4 files changed

+68
-39
lines changed

kotlinx-coroutines-core/concurrent/src/MultithreadedDispatchers.common.kt

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,40 @@
22
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5+
@file:JvmMultifileClass
6+
@file:JvmName("ThreadPoolDispatcherKt")
57
package kotlinx.coroutines
68

9+
import kotlin.jvm.*
10+
11+
/**
12+
* Creates a coroutine execution context using a single thread with built-in [yield] support.
13+
* **NOTE: The resulting [CloseableCoroutineDispatcher] owns native resources (its thread).
14+
* Resources are reclaimed by [CloseableCoroutineDispatcher.close].**
15+
*
16+
* If the resulting dispatcher is [closed][CloseableCoroutineDispatcher.close] and
17+
* attempt to submit a task is made, then:
18+
* * On the JVM, the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the
19+
* [Dispatchers.IO], so that the affected coroutine can clean up its resources and promptly complete.
20+
* * On Native, the attempt to submit a task throws an exception.
21+
*
22+
* This is a **delicate** API. The result of this method is a closeable resource with the
23+
* associated native resources (threads or native workers). It should not be allocated in place,
24+
* should be closed at the end of its lifecycle, and has non-trivial memory and CPU footprint.
25+
* If you do not need a separate thread-pool, but only have to limit effective parallelism of the dispatcher,
26+
* it is recommended to use [CoroutineDispatcher.limitedParallelism] instead.
27+
*
28+
* If you need a completely separate thread-pool with scheduling policy that is based on the standard
29+
* JDK executors, use the following expression:
30+
* `Executors.newSingleThreadExecutor().asCoroutineDispatcher()`.
31+
* See `Executor.asCoroutineDispatcher` for details.
32+
*
33+
* @param name the base name of the created thread.
34+
*/
735
@ExperimentalCoroutinesApi
8-
public expect fun newSingleThreadContext(name: String): CloseableCoroutineDispatcher
36+
@DelicateCoroutinesApi
37+
public fun newSingleThreadContext(name: String): CloseableCoroutineDispatcher =
38+
newFixedThreadPoolContext(1, name)
939

1040
@ExperimentalCoroutinesApi
1141
public expect fun newFixedThreadPoolContext(nThreads: Int, name: String): CloseableCoroutineDispatcher

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

Lines changed: 2 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,13 @@
22
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

5+
@file:JvmMultifileClass
6+
@file:JvmName("ThreadPoolDispatcherKt")
57
package kotlinx.coroutines
68

79
import java.util.concurrent.*
810
import java.util.concurrent.atomic.AtomicInteger
911

10-
/**
11-
* Creates a coroutine execution context using a single thread with built-in [yield] support.
12-
* **NOTE: The resulting [ExecutorCoroutineDispatcher] owns native resources (its thread).
13-
* Resources are reclaimed by [ExecutorCoroutineDispatcher.close].**
14-
*
15-
* If the resulting dispatcher is [closed][ExecutorCoroutineDispatcher.close] and
16-
* attempt to submit a continuation task is made,
17-
* then the [Job] of the affected task is [cancelled][Job.cancel] and the task is submitted to the
18-
* [Dispatchers.IO], so that the affected coroutine can cleanup its resources and promptly complete.
19-
*
20-
* This is a **delicate** API. The result of this method is a closeable resource with the
21-
* associated native resources (threads). It should not be allocated in place,
22-
* should be closed at the end of its lifecycle, and has non-trivial memory and CPU footprint.
23-
* If you do not need a separate thread-pool, but only have to limit effective parallelism of the dispatcher,
24-
* it is recommended to use [CoroutineDispatcher.limitedParallelism] instead.
25-
*
26-
* If you need a completely separate thread-pool with scheduling policy that is based on the standard
27-
* JDK executors, use the following expression:
28-
* `Executors.newSingleThreadExecutor().asCoroutineDispatcher()`.
29-
* See [Executor.asCoroutineDispatcher] for details.
30-
*
31-
* @param name the base name of the created thread.
32-
*/
33-
@DelicateCoroutinesApi
34-
public actual fun newSingleThreadContext(name: String): ExecutorCoroutineDispatcher =
35-
newFixedThreadPoolContext(1, name)
36-
3712
/**
3813
* Creates a coroutine execution context with the fixed-size thread-pool and built-in [yield] support.
3914
* **NOTE: The resulting [ExecutorCoroutineDispatcher] owns native resources (its threads).

kotlinx-coroutines-core/native/src/MultithreadedDispatchers.kt

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,15 @@ import kotlinx.coroutines.channels.*
99
import kotlinx.coroutines.internal.*
1010
import kotlin.coroutines.*
1111
import kotlin.native.concurrent.*
12-
13-
@ExperimentalCoroutinesApi
14-
public actual fun newSingleThreadContext(name: String): CloseableCoroutineDispatcher {
15-
return WorkerDispatcher(name)
16-
}
12+
import kotlin.time.*
13+
import kotlin.time.Duration.Companion.milliseconds
1714

1815
public actual fun newFixedThreadPoolContext(nThreads: Int, name: String): CloseableCoroutineDispatcher {
1916
require(nThreads >= 1) { "Expected at least one thread, but got: $nThreads" }
2017
return MultiWorkerDispatcher(name, nThreads)
2118
}
2219

20+
@OptIn(ExperimentalTime::class)
2321
internal class WorkerDispatcher(name: String) : CloseableCoroutineDispatcher(), Delay {
2422
private val worker = Worker.start(name = name)
2523

@@ -52,21 +50,30 @@ internal class WorkerDispatcher(name: String) : CloseableCoroutineDispatcher(),
5250
override fun dispose() {
5351
disposableHolder.value = null
5452
}
53+
54+
fun isDisposed() = disposableHolder.value == null
55+
}
56+
57+
fun Worker.runAfterDelay(block: DisposableBlock, targetMoment: TimeMark) {
58+
if (block.isDisposed()) return
59+
val durationUntilTarget = -targetMoment.elapsedNow()
60+
val quantum = 100.milliseconds
61+
if (durationUntilTarget > quantum) {
62+
executeAfter(quantum.inWholeMicroseconds) { runAfterDelay(block, targetMoment) }
63+
} else {
64+
executeAfter(maxOf(0, durationUntilTarget.inWholeMicroseconds), block)
65+
}
5566
}
5667

5768
val disposableBlock = DisposableBlock(block)
58-
worker.executeAfter(timeMillis.toMicrosSafe(), disposableBlock)
69+
val targetMoment = TimeSource.Monotonic.markNow() + timeMillis.milliseconds
70+
worker.runAfterDelay(disposableBlock, targetMoment)
5971
return disposableBlock
6072
}
6173

6274
override fun close() {
6375
worker.requestTermination().result // Note: calling "result" blocks
6476
}
65-
66-
private fun Long.toMicrosSafe(): Long {
67-
val result = this * 1000
68-
return if (result > this) result else Long.MAX_VALUE
69-
}
7077
}
7178

7279
private class MultiWorkerDispatcher(

kotlinx-coroutines-core/native/test/MultithreadedDispatchersTest.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import kotlinx.coroutines.channels.*
99
import kotlinx.coroutines.internal.*
1010
import kotlin.native.concurrent.*
1111
import kotlin.test.*
12+
import kotlin.time.Duration.Companion.seconds
1213

1314
private class BlockingBarrier(val n: Int) {
1415
val counter = atomic(0)
@@ -63,4 +64,20 @@ class MultithreadedDispatchersTest {
6364
dispatcher.close()
6465
}
6566
}
67+
68+
/**
69+
* Test that [newSingleThreadContext] will not wait for the cancelled scheduled coroutines before closing.
70+
*/
71+
@Test
72+
fun timeoutsNotPreventingClosing(): Unit = runBlocking {
73+
val dispatcher = WorkerDispatcher("test")
74+
withContext(dispatcher) {
75+
withTimeout(5.seconds) {
76+
}
77+
}
78+
withTimeout(1.seconds) {
79+
dispatcher.close() // should not wait for the timeout
80+
yield()
81+
}
82+
}
6683
}

0 commit comments

Comments
 (0)