-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Ensure Dispatchers.Main != Dispatchers.Main.immediate on Android #3924
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
@@ -132,4 +132,11 @@ class HandlerDispatcherTest : TestBase() { | |||
mainLooper.scheduler.advanceBy(51, TimeUnit.MILLISECONDS) | |||
finish(5) | |||
} | |||
|
|||
@Test | |||
fun testHandlerDispatcherNotEqualToImmediate() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This change changes the behaviour of the following code:
@Test
fun foo() = runBlocking<Unit> {
withContext(Dispatchers.Main) {
println("1")
withContext(Dispatchers.Main.immediate) {
println("2")
launch(Dispatchers.Main) {
println("4")
}
withContext(Dispatchers.Main) {
println("3")
}
}
}
}
It seems like this is expected (at least from the issue description), and I would rather expect the current (after your change) behaviour, but still pointing it out just in case
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh. Didn't realize this. Then, we'll have to weigh the pros and cons.
I checked all the other Dispatchers.Main.immediate
implementations, and they all use object identity for equality checks, so this change is also needed simply for consistency.
Also, before this change, the behavior of
withContext(Dispatchers.Main.immediate) {
println("2")
launch(Dispatchers.Main) {
println("4")
}
withContext(Dispatchers.Main) {
println("3")
}
}
was 2, 4, 3
if it was not called from the main thread, but 2, 3, 4
it it was. Now, it's consistently 2, 4, 3
. I think it's more intuitive that the calling thread doesn't affect the ordering.
Also, yes, I, too, would expect the proposed behavior:
fun updateUi() = scope.launch(Dispatchers.Main) { }
suspend fun readUiElement(): Int = withContext(Dispatchers.Main) { 1 }
fun update() = viewModelScope.launch {
updateUi()
cache = readUiElement()
}
I think it's reasonable to expect that readUiElement
detects the results of updateUi
. Currently, for this to work reliably, I think a yield
would need to be inserted between the two lines.
... That said, of course, this will subtly break someone's code that relied on the existing implicit ordering.
I'd say the pros outweigh the cons. Most people would be better served by having the execution order roughly the same as the order of function invocations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Then it's worth doing the following:
- Fix this behaviour with tests explicitly (in all integrations)
- Maybe document it around
immediate
property - Rephrase the issue (or create a new one) in a bit more clear manner so we'll reference it in the changelog properly
It's okay to do it both in and out of the scope of this issue. WDYT?
459673b
to
bfbdca9
Compare
From the latest build failure:
and
Oh, mighty computing gods, please forgive me for all my sins! I repent twice, thrice, and however many times is needed. I knew I shouldn't have written that compile-time VM with the C++ templates at the university! I implore you, please, spare me and allow me to proceed with this humble pull request. Amen. |
Behold, thou shalt not ignore the tests, lest thy build shell not pass! The first failure is penned by those who have sinned. Thou shalt not seek it -- the sorrow shalt thou find in Gradle |
The intention here must have been for `immediate` to be a property without a backing field, lazily initializing `_immediate`, but instead, `_immediate` was never meaningfully accessed.
44e1ffd
to
b25512f
Compare
98375d5
to
298f0d7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice one
override fun equals(other: Any?): Boolean = | ||
other is HandlerContext && other.handler === handler && other.invokeImmediately == invokeImmediately | ||
// inlining `Boolean.hashCode()` for Android compatibility, as requested by Animal Sniffer | ||
override fun hashCode(): Int = System.identityHashCode(handler) xor if (invokeImmediately) 1231 else 1237 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder why you've used xor
instead of IDEA-generated polynomial hash (31 * ...
).
No strong opinion here though
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These are some tough questions you're asking. Let's look at whether collisions are possible.
1231 + (1 - invokeImmediately1) * 6 + 31 * handler1 == 1231 + (1 - invokeImmediately2) * 6 + 31 * handler2 (mod 2^32)
If invokeImmediately1 == invokeImmediately2
, then handler1 == handler2 (mod 2^32)
, by the virtue of all operations here being bijective modulo 2^64. If they are different, we get
1237 + 31 * handler1 == 1231 + 31 * handler2 (mod 2^32)
or
31 * (handler2 - handler1) == 6 (mod 2^32)
This is possible if handler2 - handler1 == 1_939_662_650
.
With handler1, handler2 in [0; 2^32)
, we get around 2^32 - 1_939_662_650 == 2_355_304_646
unordered pairs where this can happen, or 2.5 * 10^(-10)
chance if they are distributed uniformly.
Let's look at
handler1 xor (1231 + (1 - invokeImmediately1) * 6) == handler2 xor (1231 + (1 - invokeImmediately2) * 6)
No modulo arithmetic this time. Again, handler1 == handler2
if invokeImmediately1 == invokeImmediately2
. Otherwise,
handler1 xor 1237 == handler2 xor 1231
Equivalently,
handler1 xor handler2 == 1237 xor 1231 == 0b11010
So, the collision will happen exactly if handler1
and handler2
differ in exactly the bits 2, 4, and 5. For every number in [0; 2^32)
, there's exactly one pair for it that causes a collision, so there are about 2^31 == 2_147_483_648
unordered pairs where this can happen, or 2.3 * 10^(-10)
chance if handler1
and handler2
are distributed uniformly.
Intuitively, a number like 2_000_000_000 can collide with two numbers in the polynomial hash but with only one in the xor
hash, and every number clashes with at least one other number in both cases.
So, the xor
hash has insignificantly better theoretical properties for the case when the colliding objects are both HandlerDispatcher
instances.
What led me to using it, however, was that it was a bit shorter to write while still keeping the bijective properties when any parameter is fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Worth noting that choosing some other constant for the polynomial hash could lead us to a much better situation like only a handful of collisions being possible at all. I don't think it's worth it to search for the required constants, though, as no one will ever be sad that their HashSet<MainCoroutineDispatcher>
on Android has a 2 * 10^(-10)
collision chance for every pair.
Fixes #3545
Fixes #3963