Closed Khang-NT closed 6 years ago
Please elaborate why do you need it.
Maybe it's worth to make HandlerContext
open
instead?
I want to consume values of a ReceiveChannel
in UI
context.
launch(UI) {
receiveChannel.consumeEach { value ->
// stuff
}
}
In the other side, SendChannel
can send()
on either main thread or background thread, I expect that if send(value)
is invoked on main thread and alwaysDispatch == false
, the value
will pass to consumer immediately without dispatching to next event loop cycle.
I'm not sure 100% if isDispatchNeeded = false
then the value
will pass to consumer immediately if both send()
and receive()
are called on main thread. But I need that behavior to make coroutine channel acting like Android LiveData
in my library:
https://github.com/Khang-NT/Kotlin-Coroutines-Lifecycle-Aware
(LiveData#setValue(newValue)
will dispatch newValue
immediately to all observers in same event loop.)
Instead of extending HandlerContext
you can use delegation.
// You can name it UI or ConditionalUI or whatever
object ConditionalDispatcher : CoroutineDispatcher() {
override fun dispatch(context: CoroutineContext, block: Runnable) = UI.dispatch(context, block)
override fun isDispatchNeeded(context: CoroutineContext): Boolean {
return Looper.myLooper() !== Looper.getMainLooper()
}
}
Does it solve your problem?
I also think of some solutions can solve my problem.
With my opinion, my problem is quite general so I open this issue whether we can official support within the library.
Related to #258
We can't implement it as is, because accidental usage of such dispatcher will lead to spurious StackOverflowError
reproducible sometimes.
We should provide something more explicit, e.g. new coroutine start mode or additional extension like UI.unconfined
or UI.immediate
.
Feel free to provide your option, the only requirement is that in place invocation should be explicit
I agree dispatching is somehow difficult to apprehend, and explicit dispatching option is mandatory.
I ended up doing a dedicated function to easily manage dispatching in UI:
fun uiJob(dispatch: Boolean = uiDispatch, block: suspend CoroutineScope.() -> Unit) : Job {
return launch(UI, if (dispatch) CoroutineStart.DEFAULT else CoroutineStart.UNDISPATCHED, block = block)
}
With uiDispatch
returns true if current thread is not UI thread. So I can explicitely select dispatching or let it automatic.
Would rather be a kotlinx-coroutines(-android) helper
Thanks for the feedback! Your workaround works well for your use-case, but it's hard for the library to maintain it: we have a lot of dispatchers, a lot of builders and a lot of [default] parameters.
I'll try to prototype UI.some_extension
as it's much easier to generalize
That would be great, I could get rid of my workaround :)
@qwwdfsad I like the UI.some_extension
design. I personally currently have a launchInUi(…)
extension method, similar to @GeoffreyMetais's one, except it throws if not called on the UI thread, so there's no doubt about whether it's executed immediately or dispatched. It's started immediately, without initial dispatch, or fails fast.
About the technique to check the UI thread, I dislike using Looper
comparison because Lopper.mainLooper()
has a synchronized
block (that is not really needed), and Looper.myLooper()
does a ThreadLocal
lookup.
This is some overhead that can be saved by caching the ui thread to a field, and just check it over the current thread, as I do here: https://github.com/LouisCAD/Splitties/blob/beb45da8ede57383aec489ac9d5630e79db60cc9/uithread/src/main/java/splitties/uithread/UiThread.kt#L30
This might be orthogonal to some of the issues you're trying to address here but...
If the primary issue is latency between posted Handler
messages you might look first at skipping the vsync barriers that delay messages until after a pending frame. (The actual Looper message processing is quite fast on its own.) The api Handler.createAsync was added in API 28 to make this easier. Messages posted to an async handler are still ordered relative to one another, but they do not wait for pending frames to measure/layout/draw before running.
The constructor it uses has been there in AOSP nearly since the vsync barriers debuted in Jellybean so a hacky backport is possible in a support/androidx lib, we just haven't added it there yet. You can do your own for testing with something like this:
private val asyncHandlerConstructor = try {
Handler::class.java.getConstructor(
Looper::class.java,
Handler.Callback::class.java,
Boolean::class.java
)
} catch (t: Throwable) {
null
}
fun createAsyncHandler(
looper: Looper = Looper.myLooper(),
callback: Handler.Callback? = null
): Handler = when {
Build.VERSION.SDK_INT >= 28 -> Handler.createAsync(looper, callback)
Build.VERSION.SDK_INT >= 17 -> asyncHandlerConstructor?.newInstance(looper, callback, true)
?: Handler(looper, callback).also {
Log.d("createAsyncHandler", "Couldn't create Handler as async!")
}
else -> Handler(looper, callback)
}
Then you can construct a HandlerContext with an async handler for the main thread that doesn't get blocked by UI operations:
val Main = HandlerContext(createAsyncHandler(Looper.getMainLooper()), "main")
Some folks working with RxAndroid saw some significant performance/latency benefits from this in testing and there's a PR over there in progress to use the same underlying mechanism: https://github.com/ReactiveX/RxAndroid/pull/416
@LouisCAD Do you have any performance numbers? Was this check so expensive that you've explicitly rewritten your code to mitigate it?
I don't mind additionally optimizing it, but it's good to know it's worth it. Looper
comparison is user-friendly: even if javadoc/name is unclear, it's easy to understand what's going on in implementation after looking at single check, while solution with caching threads doesn't have this property.
@adamp Thanks! Let's see how this PR is going. One already can create coroutines dispatcher over any Handler
whether it's async or not. If it's really useful, we can provide global constant like UI
which uses async handler over main looper, name is the only problem :)
It might be better in the long term to make the UI
dispatcher async by default.
The whole vsync barrier thing was a requirement for us to keep behavioral compatibility with a lot of existing code at the time but it's almost never what you want in new code and the latency hit is pretty huge. The pattern it was made to protect is this:
myView.setText("some text")
myView.post { doSomethingWith(myView.width) }
Where the width
of a view isn't updated to reflect the newly set text until after measure/layout. Prior to vsync in jellybean requestLayout
as performed by setText
was implemented as a simple handler post
so the operations were serialized.
I can't think of a good reason why someone would write a suspending version of something like TextView.setText
that resumed after the frame such that this behavior would need to be preserved for the UI
dispatcher. (And if they did, they could resume based on a post to a non-async Handler internally anyway.) Since the coroutines libs are still experimental it would be nice to push people toward dispatchers that skip them by default before there's a bunch of legacy code to think about. It's too bad RxAndroid can't go back in time and make this the default too.
Created #427 for discussing it
@qwwdfsad I had written tests for this before taking my decision: https://github.com/LouisCAD/Splitties/blob/beb45da8ede57383aec489ac9d5630e79db60cc9/checkedlazy/src/androidTest/java/splitties/checkedlazy/PerformanceTest.kt
Regarding the original problem - getting dispatch behavior similar to LiveData
where downstream behavior can interrupt or otherwise affect the rest of the dispatch - it sounds like you either want observers that need to affect the dispatch to use Unconfined
or the wrapped dispatcher proposed above. However, using anything other than Unconfined
here means that under some circumstances that might not be easily reproduced, your observer won't be able to affect the upstream dispatch, but it might assume it can. That sounds like a recipe for bugs.
It also looks like skipping dispatch in the manner proposed here also breaks some ordering guarantees. Consider:
If message B and C were the result of dequeuing some sort of work from the same queue, they're now out of order. This might happen many layers deep in the associated code and be very difficult to track.
A number of RxJava users in the Android community have written Schedulers that behave this way and got bit by ordering bugs like the above. The answer on that side of things has usually been either, "don't use observeOn if you need it to run synchronously" (Unconfined
) or to address the latency issues that come from the vsync barriers if that's the motivation.
If you want Message B to run after Message A and before Message C, with coroutines, you just have to launch a single coroutine, which executes sequentially (i.e. in order), instead of launching multiple coroutines. The syntax of coroutines make it hard to have a bug like you describe unintentionally, regardless of whether there's dispatch or not.
On Mon, Jul 9, 2018, 8:19 PM Adam Powell notifications@github.com wrote:
Regarding the original problem - getting dispatch behavior similar to LiveData where downstream behavior can interrupt or otherwise affect the rest of the dispatch - it sounds like you either want observers that need to affect the dispatch to use Unconfined or the wrapped dispatcher proposed above. However, using anything other than Unconfined here means that under some circumstances that might not be easily reproduced, your observer won't be able to affect the upstream dispatch, but it might assume it can. That sounds like a recipe for bugs.
It also looks like skipping dispatch in the manner proposed here also breaks some ordering guarantees. Consider:
- Message A is posted to the Handler
- Message B is posted to the Handler
- Message A executes, performs some operation that results in processing and dispatch of Message C
- Message C skips dispatch and executes immediately since it's on the main thread already
- Message B executes after Message C has executed
If message B and C were the result of dequeuing some sort of work from the same queue, they're now out of order. This might happen many layers deep in the associated code and be very difficult to track.
A number of RxJava users in the Android community have written Schedulers that behave this way and got bit by ordering bugs like the above. The answer on that side of things has usually been either, "don't use observeOn if you need it to run synchronously" (Unconfined) or to address the latency issues that come from the vsync barriers if that's the motivation.
— You are receiving this because you were mentioned. Reply to this email directly, view it on GitHub https://github.com/Kotlin/kotlinx.coroutines/issues/381#issuecomment-403572936, or mute the thread https://github.com/notifications/unsubscribe-auth/AGpvBaio7HbCr0QY2qoQa_WfEVpffwgaks5uE56UgaJpZM4UYMo5 .
Nothing about coroutines precludes the ability to have the above problem. The use a single coroutine represents only a fraction of the interaction with a single-threaded scheduler.
It's harder, for sure. It could still interact poorly with other code a couple layers away though, and giving it prominent placement in the API could lead people to think that it's a general purpose, "go faster" button when it may not be addressing the right problems.
It seems like the example code at the beginning of the thread assumes that the channel consumer will fully finish processing the item before the producer continues dispatching rather than just receive the item from the channel, this being the LiveData
behavior described. Without an API guarantee that the dispatch won't continue until after processing of the item and not just receipt, relying on unconfined dispatch behavior alone to do this seems brittle.
Could we change implementation of method
HandlerContext.isDispatchNeeded()
to be more flexible: