0

In Java Android, to achieve sequential behavior without blocking main thread, this is the code I am using.

Java Android

private static final ExecutorService executor = Executors.newSingleThreadExecutor();

executor.execute(() -> task0());
executor.execute(() -> task1());
executor.execute(() -> task2());

The above code, will always execute in the exact order of task0, task1 and task2 functions regardless what is happening inside the functions.

I am impressed by the life cycle aware feature, offered by Kotlin's LifecycleScope. I try to write the code in the following Kotlin's LifecycleScope form.

Kotlin Android

val dispatcherForCalendar = Executors.newSingleThreadExecutor().asCoroutineDispatcher()

lifecycleScope.launch(dispatcherForCalendar) {
    task0()
}
lifecycleScope.launch(dispatcherForCalendar) {
    task1()
}
lifecycleScope.launch(dispatcherForCalendar) {
    task2()
}

The above code will executed in the exact order of task0, task1 and task2 functions, except when delay is performed in the functions.

  1. In reality, I will not insert delay code explicitly in task functions. In such a case, can Android system still perform delay operation implicitly?
  2. How I can achieve sequential behavior, same as my Java code Executors.newSingleThreadExecutor?

Thank you.

Cheok Yan Cheng
  • 47,586
  • 132
  • 466
  • 875
  • 1
    I don't believe such a feature exists in Kotlin, at least not built in. – Louis Wasserman Jun 21 '23 at 17:00
  • 2
    Why can't you put all 3 calls in a single `launch()`? – broot Jun 21 '23 at 17:13
  • This is just a simplified code snippet to convey the idea. In real use case, task1() might only happen after user has performed some action like button clicked. – Cheok Yan Cheng Jun 21 '23 at 18:59
  • I just have problems understanding your use case. If it is guaranteed `task1()` is submitted only after `task0()` finished, then we don't need any synchronization. If we don't have such guarantees, so submitters generally run asynchronously to each other, then `task1()` may be submitted even before `task0()`. So what are guarantees on when `task1()` is submitted in relation to `task0()`? Do you mean that while submitting `task0()` we should get an "acknowledge" and it is guaranteed only after such acknowledge `task1()` could be submitted? – broot Jun 21 '23 at 19:32
  • `task1` can be submitted in anytime. It can be submitted before `task0` begin to execute, it can be submitted while `task0` is executing, or it can be submitted after `task0` is finished executed. – Cheok Yan Cheng Jun 21 '23 at 19:50
  • And if `task1()` is submitted before `task0()` should it still wait for `task0()` before starting? – broot Jun 21 '23 at 19:56
  • `task1` will always submitted after `task0` is submitted. but, after `task0` is submitted, that doesn't mean `task0` has begun execution. Our expectation is `task1` only execute after `task0` finishes execution. It seems like coroutine not able to provide such a guarantee. – Cheok Yan Cheng Jun 21 '23 at 20:03

1 Answers1

1

Regarding 1):

The coroutines may yield the thread back to the dispatcher at any suspend function call, not just at delay() calls.

Regarding 2):

My answer here shows how you can build a sequential task queue using a Channel. You could modify the class to allow a CoroutineScope to be injected in the constructor, so lifecycleScope could be passed in when using it in an Activity or Fragment. Something like this:

class JobQueue(
    private val scope: CoroutineScope,
    private val defaultContext: CoroutineContext = Dispatchers.Main
) {
    private val queue = Channel<Job>(Channel.UNLIMITED)

    init { 
        scope.launch(Dispatchers.Default) {
            for (job in queue) job.join()
        }
    }

    fun submit(
        context: CoroutineContext = defaultContext,
        block: suspend CoroutineScope.() -> Unit
    ) {
        synchronized { 
            val job = scope.launch(context, CoroutineStart.LAZY, block)
            queue.trySend(job)
        }
    }
}

If these were blocking jobs, you might for example pass Dispatchers.IO as the defaultContext for running the jobs.

Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Depending on desired failure behavior, you might want to wrap `job.join()` in a try/catch. – Tenfour04 Jun 21 '23 at 17:13
  • Sorry. I do not understand "yield the thread back to the dispatcher". Can you kindly explain more? – Cheok Yan Cheng Jun 22 '23 at 18:11
  • Dispatchers are basically thread pools. A coroutine at the low level is made up of a series of Continuations. The breaks between continuations are at suspend function calls. At these points, the coroutine returns (yields) the thread back to the pool. When it resumes from suspension, it is lent a thread from a dispatcher again. – Tenfour04 Jun 22 '23 at 18:30
  • I was assuming your question 1 is wondering if your way of doing it would successfully preserve coroutine sequence if you didn’t explicitly call `delay()` and I’m saying it will not preserve the sequence if you have any suspend function calls at all. – Tenfour04 Jun 22 '23 at 18:33