-
Notifications
You must be signed in to change notification settings - Fork 1.9k
Add option to yield(yieldLevel = EVENT_LOOP)
to yield undispatched to the Event Loop queue for an immediate
dispatcher.
#4456
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
Comments
I don't consider relying on the Dispatcher's specific behavior a safe approach for implementing thread synchronization.
I disagree. In general, code without explicit synchronization should not interfere with CPU scheduling.
This proposal is too implementation-specific. Even if it works, user code would require very careful documentation, and it could be extremely difficult to find and fix bugs that arise as the user's code evolves. |
result = select {
onReceive() ... for all nodes
onTimeout(0) {
ActionsExhausted
} It looks like: result = channels.asSequence()
.mapNotNull { it.tryReceive().getOrNull() }
.firstOrNull()
?: ActionsExhausted |
Yes this is a good point if I can get those channels filled! But without this behaviour the channels won't be filled. |
For "you should consider using explicit synchronization mechanisms" - that is not always possible as a library author whose API invites users to write their own coroutines. I do not want to force them into a particular complex use of a synchronization mechanism. For "I don't consider relying on the Dispatcher's specific behavior a safe approach for implementing thread synchronization." This is not thread synchronization per se (its all on the same thread!). Also, That is directly contradictory with the shape of the library as is, no? What I'm asking for here is consistency to extend this special "immediate" behaviour to another surface where it has been currently ignored. Does that make sense?
So do you have another implementation to propose for event loop yielding for immediate dispatchers? I'm not sure what this push back is - I thought offering an implementation would be helpful? |
A few other thoughts as I continue to reflect on this.
|
True, the same documentation explains the pitfall of using |
Yes I know, I'm not requiring it! If it has not happened, we move on. I am rather trying to give the other coroutines an opportunity to run. This reads to me exactly what @qwwdfsad was specifying in the original question of this matter here: #737 . Except that it was only implemented for the |
Going back, it looks like this was actually the original behaviour for "immediate" dispatchers:
It was requested as an enhancement to allow the Which, btw, I think is great and makes total sense. We should by default post to thread for But the original behaviour there for |
There is a reason to use Is the following code reasonable for you? withContext(Main) {
result = select { /* ... */ }
} |
No, I need the state machine framework to stabilize in response to an event within 1 post to the Handler (1 main thread message). the framework first resumes from suspension to respond to that event from the original I now need to optimize it to go ahead and let any other 'immediate' coroutines run (these were launched by client code on the runtime controlled |
I tested this with my local version of coroutines and it looks like they are separate event loops as @dkhalanskyjb was suggesting on slack. Which means that even with my suggested API change this won't fix the problem. This has helped me learn a lot more about exactly how the event loop is implemented - within 1 thread and 1 call hierarchy. |
I will close this issue as yielding to the event loop is less effective than I had hoped. I still think its a reasonable ask for immediate dispatchers, but it likely has very utility. |
Uh oh!
There was an error while loading. Please reload this page.
Use case
Issue in Library Repo: square/workflow-kotlin#1311
Quick Description:
In Workflow, we execute a runtime of state machines that process actions in response to asynchronous events. This runtime executes on the
Main.immediate
dispatcher on Android. We rely on the fact that this declarative, UDF framework runtime can respond to any one 'event' in a single Main Thread Message/Task (that is, without posting anything theHandler
).There are nodes within the tree that are collecting flows and creating actions that cause a state change as a result. The runtime loop waits for actions on these nodes by building a
select
statement across all of the nodes withonReceive
clauses for a channel on each of the nodes.Currently we execute one action per loop of the runtime, but we are seeking to optimize this for performance reasons.
When there are multiple actions that can be processed before the updated ViewModel is passed to UI code, we wish to process all of them. To do that we loop again on the
select
statement but with a "last place"onTimeout(0)
to ensure we are doing this immediately.E.g.,
Then we go ahead and pass the 'fresh' ViewModel to view code.
Since the collection of actions and the processing of the event loop all happens consecutively on the
Main.immediate
dispatcher, when we resume from the firstselect
statement, we continue on without giving the other actions a chance to be sent to their channels (their continuations need to be resumed for that to happen!). This prevents us from being able to achieve this optimization of processing multiple actions immediately, even though they are all 'immediately' available.The following works to achieve the desired behaviour:
I have attempted to use this method and try to adjust the rest of the semantics to happen across multiple main thread messages, since
yield()
ends up posting to the Main thread Handler even withMain.immediate
. Ultimately after a month of attempts, this was untenable. The behaviour of "1 event" = "1 main thread message/task" is too valuable for performance measurement, logic reasoning, etc.I do not wish to write my own
CoroutineDispatcher
since too much of the valuable functionality isinternal
and I would need to re-write it unnecessarily.I was surprised when I first discovered that the
Main.immediate
dispatcher would always post to the Handler,, instead thinking that, since it shared an Event Loop implementation withDispatchers.Unconfined
it might likewise share theyield()
implementation toyield()
to the event loop queue). Original kotlinlang post - https://kotlinlang.slack.com/archives/C1CFAFJSK/p1745528199117359@dkhalanskyjb convinced me that we definitely do not want to make this the default behaviour of
yield()
since that would be a surprise when trying to guarantee liveness at the level of the Main thread/Handler and not just the Event Loop.However, I still think this is a reasonable API to request for completeness for "immediate" dispatcher behaviour in this use case. We should be able to drain the EventLoop continuations immediately before trying the
select
again.Another way to succinctly reproduce this request (albeit in a much less reasonable form factor) is with the following test that only succeeds on Android
Main.immediate
whenyield()
is used:I want to be able to do ^that in a single main thread message / handler task.
The Shape of the API
I propose the addition of the
yieldLevel
optional parameter. Something like the following:Prior Art
Obviously there is already some prior art for this type of opt-in special behaviour with the
CoroutineStart
parameter on coroutine builders.Also the underlying implementation for
yieldUndispatched()
already exists for theUnconfined
dispatcher and can be used as well for theimmediate
dispatcher just likeexecuteUnconfined()
is used when doing immediate dispatch.Lastly,
yield()
is acceptable to guarantee liveness in tight loops, and this is one of those scenarios, it is just localized only to "liveness" within the EventLoop (maybe more accurately "fairness" since we are not talking about UI response) - so why not allow us to bring "liveness"/"fairness" just to those continuations?The text was updated successfully, but these errors were encountered: