Hello, I am trying to implement some feature where...
# coroutines
h
Hello, I am trying to implement some feature where one suspending function is waiting for completion of a
CompletableJob
before continuing its work (updating a state). Simultaneously there is another (suspending) function (triggered from the UI for example) which completes that CompletableJob once its called. So far so good, but now I want this second function to only return once the first suspending function had a chance to do its work I've written a simple test to demonstrate my problems, and my solution so far. My solution now is to call
yield()
in the second suspending function, to give the first suspending function back control to do its work. This works in a simple case, but once both functions are included in more complex codebases i got to a situation where the first function was wrapped in another lauch method, and in that case just doing 1
yield()
is not enough, in that case I needed to do 2
yield()
calls. I was wondering if there is another solution? Ideally without one or many yield calls. I've attached the samples in the thread (4 unit tests) Note that this is a very simplified example, in my real world example its not a double join, but a join in a function thats wrapped in an async statement which is wrapped in a launch statement divided over sevaral layers / classes
example tests.cpp
j
Without the real-life scenario it's hard to know whether this is an XY problem (so be careful about this!). Just for the sake of mentioning it, have you considered using a simple
CompletableJob
for the other synchronization instead of setting a boolean?
Also, you don't need the
coroutineScope { ... }
in the first coroutine,
launch
already gives you a scope and already waits for children
h
Thanks for replying. My real current implementation is I think what you described as using a CompletableJob for the other syncronisation. After method 2 completes the CompletableJob, it awaits for the state change it expects, but I was hoping there was some sort of an eager await or eager async mechanism that would directly run as soon as possible, so its less error prone due to forgetting to wait for the expected state change
And i added the coroutineScope in the first coroutine because in my real world example I needed it to do the nested async / launch call and check if the coroutine is still active (and in my real world its not directly in a launch but a suspending function call)
s
If the work in the first coroutine is fast and non-suspending, you could attach it to the
completableJob
using
invokeOnCompletion
. That way it will run synchronously during the call to
complete()
, and when
complete()
returns it is guaranteed to have finished. I think that's the only way to get your current design to work. If you can't use that approach, you may need to redesign the code to include another way for the two jobs to communicate, like another
Job
or a
Channel
.
I was thinking maybe it would be possible to exploit the job parent/child relationships, and somehow link the
launch
job to the
completableJob
, but that gets a bit too messy for my liking.
f
If your second coroutine is effectively starting the other coroutine (by completing the job) and waiting for it to complete, why not calling it directly from there?
Copy code
// instead of 
// completableJob.complete()
// yield()
// call directly 
doStuff()
// or start on another scope
scope.launch {
  doStuff()
}.join()
if you cannot model it like this and need a shared state, then use a StateFlow
Copy code
val stateOfTheStuff = MutableStateFlow(NOT_INVOKED_YET)
launch {
  stateOfTheStuff.first { it == INVOKED }
  doStuff()
  stateOfTheStuff.value = COMPLETED
}
launch {
  stateOfTheStuff.value = INVOKED
  stateOfTheStuff.first { it === COMPLETED }
  // do the rest 
}
h
@franztesca thanks, your second approach is exactly what I'm doing right now, but I was hoping to get rid of the neccesary
first
statement. Its for a state machine, and completing the job should trigger a state transition, but after completing the job, you still have to wait for the transition to be complete (so I added an
awaitTransition()
method that I use right now):
Copy code
suspend fun awaitTransition(state: T) {
    stateFlow.filter { it != state }.first()
}
It would be worthwhile to maybe link the launch job to the completablejob as @Sam sort of suggested, if i can get something like that to work, then I can consider if thats a good idea or better to not do it and make waiting for the state update manually. Sam maybe you got some hints as where I should look for your perhaps messy suggestion?
s
I gave it some thought but I haven't come up with anything much, sorry.
h
Thanks for your help and effort nevertheless. At least I concluded that my first approach is probably not going to get better anytime soon
s
For this type of coordination between 2 or more coroutines, have you considered a solution that uses Channels or Semaphores/Mutexes?