Skip to content

Commit c5ec672

Browse files
committed
Implementation of Dispatchers.Main that uses main queue on iOS and default dispatcher on other platforms (#2858)
1 parent 73cd4d7 commit c5ec672

File tree

4 files changed

+256
-9
lines changed

4 files changed

+256
-9
lines changed

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

+3-9
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,9 @@ import kotlin.coroutines.*
88

99
public actual object Dispatchers {
1010
public actual val Default: CoroutineDispatcher = createDefaultDispatcher()
11-
public actual val Main: MainCoroutineDispatcher = NativeMainDispatcher(Default)
11+
public actual val Main: MainCoroutineDispatcher = createMainDispatcher(Default)
1212
public actual val Unconfined: CoroutineDispatcher get() = kotlinx.coroutines.Unconfined // Avoid freezing
1313
}
1414

15-
private class NativeMainDispatcher(val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() {
16-
override val immediate: MainCoroutineDispatcher
17-
get() = throw UnsupportedOperationException("Immediate dispatching is not supported on Native")
18-
override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block)
19-
override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context)
20-
override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block)
21-
override fun toString(): String = toStringInternalImpl() ?: delegate.toString()
22-
}
15+
internal expect fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher
16+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines
6+
7+
import kotlinx.cinterop.*
8+
import platform.CoreFoundation.*
9+
import platform.darwin.*
10+
import kotlin.coroutines.*
11+
import kotlin.native.concurrent.*
12+
import kotlin.native.internal.NativePtr
13+
14+
internal fun isMainThread(): Boolean = CFRunLoopGetCurrent() == CFRunLoopGetMain()
15+
16+
internal actual fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher =
17+
DarwinMainDispatcher(false)
18+
19+
private class DarwinMainDispatcher(
20+
private val invokeImmediately: Boolean
21+
) : MainCoroutineDispatcher(), Delay {
22+
23+
override val immediate: MainCoroutineDispatcher =
24+
if (invokeImmediately) this else DarwinMainDispatcher(true)
25+
26+
override fun isDispatchNeeded(context: CoroutineContext): Boolean = !(invokeImmediately && isMainThread())
27+
28+
override fun dispatch(context: CoroutineContext, block: Runnable) {
29+
dispatch_async(dispatch_get_main_queue()) {
30+
block.run()
31+
}
32+
}
33+
34+
override fun scheduleResumeAfterDelay(timeMillis: Long, continuation: CancellableContinuation<Unit>) {
35+
val timer = Timer()
36+
val timerBlock: TimerBlock = {
37+
timer.dispose()
38+
continuation.resume(Unit)
39+
}
40+
timer.start(timeMillis, timerBlock)
41+
continuation.disposeOnCancellation(timer)
42+
}
43+
44+
override fun invokeOnTimeout(timeMillis: Long, block: Runnable, context: CoroutineContext): DisposableHandle {
45+
val timer = Timer()
46+
val timerBlock: TimerBlock = {
47+
timer.dispose()
48+
block.run()
49+
}
50+
timer.start(timeMillis, timerBlock)
51+
return timer
52+
}
53+
54+
override fun toString(): String =
55+
"MainDispatcher${ if(invokeImmediately) "[immediate]" else "" }"
56+
}
57+
58+
private typealias TimerBlock = (CFRunLoopTimerRef?) -> Unit
59+
60+
private val TIMER_NEW = NativePtr.NULL
61+
private val TIMER_DISPOSED = NativePtr.NULL.plus(1)
62+
63+
private class Timer : DisposableHandle {
64+
private val ref = AtomicNativePtr(TIMER_NEW)
65+
66+
fun start(timeMillis: Long, timerBlock: TimerBlock) {
67+
val fireDate = CFAbsoluteTimeGetCurrent() + timeMillis / 1000.0
68+
val timer = CFRunLoopTimerCreateWithHandler(null, fireDate, 0.0, 0u, 0, timerBlock)
69+
CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes)
70+
if (!ref.compareAndSet(TIMER_NEW, timer.rawValue)) {
71+
// dispose was already called concurrently
72+
release(timer)
73+
}
74+
}
75+
76+
override fun dispose() {
77+
while (true) {
78+
val ptr = ref.value
79+
if (ptr == TIMER_DISPOSED) return
80+
if (ref.compareAndSet(ptr, TIMER_DISPOSED)) {
81+
if (ptr != TIMER_NEW) release(interpretCPointer(ptr))
82+
return
83+
}
84+
}
85+
}
86+
87+
private fun release(timer: CFRunLoopTimerRef?) {
88+
CFRunLoopRemoveTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes)
89+
CFRelease(timer)
90+
}
91+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
/*
2+
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines
6+
7+
import kotlin.coroutines.*
8+
import kotlin.test.*
9+
import platform.CoreFoundation.*
10+
import platform.darwin.*
11+
12+
class MainDispatcherTest : TestBase() {
13+
14+
private fun isMainThread(): Boolean = CFRunLoopGetCurrent() == CFRunLoopGetMain()
15+
16+
@Test
17+
fun testDispatchNecessityCheckWithMainImmediateDispatcher() {
18+
if (isMainThread()) return
19+
runTest {
20+
val main = Dispatchers.Main.immediate
21+
assertTrue(main.isDispatchNeeded(EmptyCoroutineContext))
22+
withContext(Dispatchers.Default) {
23+
assertTrue(main.isDispatchNeeded(EmptyCoroutineContext))
24+
withContext(Dispatchers.Main) {
25+
assertFalse(main.isDispatchNeeded(EmptyCoroutineContext))
26+
}
27+
assertTrue(main.isDispatchNeeded(EmptyCoroutineContext))
28+
}
29+
}
30+
}
31+
32+
@Test
33+
fun testWithContext() {
34+
if (isMainThread()) return // skip if already on the main thread, run blocking doesn't really works well with that
35+
runTest {
36+
expect(1)
37+
assertFalse(isMainThread())
38+
withContext(Dispatchers.Main) {
39+
assertTrue(isMainThread())
40+
expect(2)
41+
}
42+
assertFalse(isMainThread())
43+
finish(3)
44+
}
45+
}
46+
47+
@Test
48+
fun testWithContextDelay() {
49+
if (isMainThread()) return // skip if already on the main thread, run blocking doesn't really works well with that
50+
runTest {
51+
expect(1)
52+
withContext(Dispatchers.Main) {
53+
assertTrue(isMainThread())
54+
expect(2)
55+
delay(100)
56+
assertTrue(isMainThread())
57+
expect(3)
58+
}
59+
assertFalse(isMainThread())
60+
finish(4)
61+
}
62+
}
63+
64+
@Test
65+
fun testWithTimeoutContextDelayNoTimeout() {
66+
if (isMainThread()) return // skip if already on the main thread, run blocking doesn't really works well with that
67+
runTest {
68+
expect(1)
69+
withTimeout(1000) {
70+
withContext(Dispatchers.Main) {
71+
assertTrue(isMainThread())
72+
expect(2)
73+
delay(100)
74+
assertTrue(isMainThread())
75+
expect(3)
76+
}
77+
}
78+
assertFalse(isMainThread())
79+
finish(4)
80+
}
81+
}
82+
83+
@Test
84+
fun testWithTimeoutContextDelayTimeout() {
85+
if (isMainThread()) return // skip if already on the main thread, run blocking doesn't really works well with that
86+
runTest {
87+
expect(1)
88+
assertFailsWith<TimeoutCancellationException> {
89+
withTimeout(100) {
90+
withContext(Dispatchers.Main) {
91+
assertTrue(isMainThread())
92+
expect(2)
93+
delay(1000)
94+
expectUnreached()
95+
}
96+
}
97+
expectUnreached()
98+
}
99+
assertFalse(isMainThread())
100+
finish(3)
101+
}
102+
}
103+
104+
@Test
105+
fun testWithContextTimeoutDelayNoTimeout() {
106+
if (isMainThread()) return // skip if already on the main thread, run blocking doesn't really works well with that
107+
runTest {
108+
expect(1)
109+
withContext(Dispatchers.Main) {
110+
withTimeout(1000) {
111+
assertTrue(isMainThread())
112+
expect(2)
113+
delay(100)
114+
assertTrue(isMainThread())
115+
expect(3)
116+
}
117+
}
118+
assertFalse(isMainThread())
119+
finish(4)
120+
}
121+
}
122+
123+
@Test
124+
fun testWithContextTimeoutDelayTimeout() {
125+
if (isMainThread()) return // skip if already on the main thread, run blocking doesn't really works well with that
126+
runTest {
127+
expect(1)
128+
assertFailsWith<TimeoutCancellationException> {
129+
withContext(Dispatchers.Main) {
130+
withTimeout(100) {
131+
assertTrue(isMainThread())
132+
expect(2)
133+
delay(1000)
134+
expectUnreached()
135+
}
136+
}
137+
expectUnreached()
138+
}
139+
assertFalse(isMainThread())
140+
finish(3)
141+
}
142+
}
143+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/*
2+
* Copyright 2016-2021 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package kotlinx.coroutines
6+
7+
import kotlin.coroutines.*
8+
9+
internal actual fun createMainDispatcher(default: CoroutineDispatcher): MainCoroutineDispatcher =
10+
NativeMainDispatcher(default)
11+
12+
private class NativeMainDispatcher(private val delegate: CoroutineDispatcher) : MainCoroutineDispatcher() {
13+
override val immediate: MainCoroutineDispatcher
14+
get() = throw UnsupportedOperationException("Immediate dispatching is not supported on this platform")
15+
override fun dispatch(context: CoroutineContext, block: Runnable) = delegate.dispatch(context, block)
16+
override fun isDispatchNeeded(context: CoroutineContext): Boolean = delegate.isDispatchNeeded(context)
17+
override fun dispatchYield(context: CoroutineContext, block: Runnable) = delegate.dispatchYield(context, block)
18+
override fun toString(): String = toStringInternalImpl() ?: delegate.toString()
19+
}

0 commit comments

Comments
 (0)