j2emanue
j2emanue

Reputation: 62519

What is the purpose of coroutine yield()?

I'm not exactly sure what is the purpose of the yield function. Can you check this example I have?

I am following an example here.

Here is the code:

val job = launch {
    val child = launch {
        try {
            delay(Long.MAX_VALUE)
        } finally {
            println("Child is cancelled")
        }
    }
    yield() //why do i need this ???????
    println("Cancelling child")
    child.cancel()
    child.join()
    yield()
    println("Parent is not cancelled")
}
job.join()

When I comment out the first yield I get the following results:

but if I leave the yield as it is I get:

What does it mean to use yield here?

Upvotes: 54

Views: 24047

Answers (10)

With the information @j2emanue provided, it seems that without the first yield(), the parent job by default would be marked the higher priority than the child job. Your code was not going to run into the child block as it should. It kept running to child.cancel() and so on. That was why you get message "Parent is not cancelled". And you never got the message "Child is cancelled". In contrary you leave yield() there, it would mark the child job to run with higher priority than the parent job. So you get the message "Child is cancelled" before "Parent is not cancelled". Correct me if I give any wrong explaination.

Upvotes: 0

Gor Madatyan
Gor Madatyan

Reputation: 75

There is all right with @j2emanue's answer, but the word thread will be replaced with coroutine. if there are more corutines than threads that current dispatcher can offer and one of coroutines call yield then it's priority decrements and it let other coroutines to be run on that thread instead of the coroutine that called yield. this example shows the described above very well:

import kotlinx.coroutines.*

@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
fun main() = runBlocking<Unit> {
    val singleThreadDispatcher = newSingleThreadContext("JustThread")

    runBlocking{
        launch(singleThreadDispatcher){
            while (true){
                println("ab")
                Thread.sleep(500)
                yield()
            }
        }

        launch(singleThreadDispatcher){
            while (true){
                println("cp")
                Thread.sleep(500)
                yield()
            }
        }
    }
}

in this example coroutines work in turn. if remove the call of yield in both places then the first coroutine just occupies the thread and don't let the second one to run at all, if we remove only the second call to yield then the first coroutine after it's first call to yield never resumes again.

Upvotes: 0

Abhijit Sarkar
Abhijit Sarkar

Reputation: 24558

@Vasile's answer is the most relevant to the question, the accepted answer from @Yuri Schimke is just general information that doesn't actually answer the question.

To illustrate the need for the first yield, let's change the code slightly by adding two "* is running" statements:

val job = launch {
    val child = launch {
        try {
            println("Child is running")
            delay(Long.MAX_VALUE)
        } finally {
            println("Child is cancelled")
        }
    }
    
    yield() // without this, child job doesn't get executed
    println("Cancelling child")
    child.cancel()
    child.join()
    yield()
    println("Parent is not cancelled")
}
println("Parent is running")
job.join()

Output:

Parent is running
Child is running
Cancelling child
Child is cancelled
Parent is not cancelled

Without the first yield, "Child is running" is never printed since the child job doesn't get a chance to run. delay suspends child execution and resumes parent execution. cancel interrupts the delay and moves the execution into the finally block. The join and the second yield have no real effect, but by calling join on the child job, we made absolutely sure that any following code is only executed once the child is completed/cancelled.

Upvotes: 21

Xuan
Xuan

Reputation: 736

I would answer the question in the context of 4 related things:

  • Sequence yield(value: T) is totally unrelated to coroutine yield()
  • isActive is just a flag to identify if the coroutine is still active or cancelled. You can check this flag periodically and decide to stop current coroutine or continue. Of course, normally, we only continue if it's true. Otherwise don't run anything or throws exception, ex. CancellationException.
  • ensureActive() checks the isActive flag above and throws CancellationException if it's false.
  • Coroutine yield() not only calls ensureActive() first, but then also politely tells other coroutines in the same dispatcher that: "Hey, you guys could go first, then I will continue." The reason could be "My job is not so important at the moment." or "I am sorry to block you guys for so long. I am not a selfish person, so it's your turn now." You can understand here exactly like this meaning in dictionary: "yield (to somebody/something): to allow vehicles on a bigger road to go first." SYNONYM: give way.

Upvotes: 29

vivek
vivek

Reputation: 1112

Read below comments in code for description:

runBlocking {

        // Main job thread in example
        val job = launch {

            // Child job thread
            val child = launch {
                try {
                    println("logTag: Child before delay")
                    delay(Long.MAX_VALUE)
                    println("logTag: Child after delay")
                } finally {
                    println("logTag: Child is cancelled")
                }
            }

            // If you remove this yield, above child job thread will never execute.
            // And before execution, child job will be cancelled via below line child.cancel().
            // Here yield() is saying that thread (main job thread) is not doing anything that important and if other threads (i.e. child job thread) need to be run, they can run.
            // Try yourself, after commenting/uncommenting this yield(), and observe response.
            yield()

            println("logTag: Cancelling... child")
            child.cancel()
            child.join()

            // This is permitting other thread need to be run, if available.
            // This yield() doing nothing because there is no more active threads available. Because child job thread already cancelled above.
            // After commenting/uncommenting this, no change in response.
            yield()

            println("logTag: Parent is not cancelled")
        }
        job.join()
    }

Upvotes: 2

j2emanue
j2emanue

Reputation: 62519

After some research, I see that the term yield is actually from computer science and the term yielding a thread is what I did not understand.

essentially: yield() basically means that the thread is not doing anything that important and if other threads need to be run, they can run. (I'd prefer to use join as Alex Yu mentioned). Basically, if we want to visualize what yield is doing... whatever thread you call yield on will get pushed to the back of the messaging queue, then other threads with the same priority get executed ahead of it. So it's like going to the back of the line at a club.

Upvotes: 33

Krishna Sony
Krishna Sony

Reputation: 1369

suspend fun yield(): Unit (source)

Yields the thread (or thread pool) of the current coroutine dispatcher to other coroutine to run if possible.

This suspending function is cancellable. If the Job of the current coroutine is canceled or completed when this suspending function is invoked or while this function is waiting for dispatch, it resumes with a CancellationException.

Note: This function always checks for cancellation even when it does not suspend.

Upvotes: -2

oiyio
oiyio

Reputation: 5925

In your example, yield() is called inside parent job. It says to your parent: "you work much more, wait please, i will let the other tasks to work for some time, after some time i will let you continue to work ".

Thus parent job waits.. Child job works some time.. After some time, parent job passes the next line which comes after yield(), and cancels the child job.

If you do not use yield() in your example, parent job immediately cancels child job.

Let me explain the yield with different example which shows yield in much more clear way. 2 jobs wait in the queue for waiting thread to let them to work. When you call yield, thread looks into the queue and sees other job waiting so it lets the other job to work.

import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.yield

fun main() = runBlocking {


    val job1 = launch {
        repeat(10) {
            delay(1000)
            println("$it. step done in job 1 ")
            yield()
        }
    }

    val job2 = launch {
        repeat(10) {
            delay(1000)
            println("$it. step done in job 2 ")
            yield()
        }
    }

    job1.join()
    job2.join()
    println("done")
}

Output:

0. step done in job 1 
0. step done in job 2 
1. step done in job 1 
1. step done in job 2 
2. step done in job 1 
2. step done in job 2 
3. step done in job 1 
3. step done in job 2 
4. step done in job 1 
4. step done in job 2 
5. step done in job 1 
5. step done in job 2 
6. step done in job 1 
6. step done in job 2 
7. step done in job 1 
7. step done in job 2 
8. step done in job 1 
8. step done in job 2 
9. step done in job 1 
9. step done in job 2 
done

Upvotes: 7

Vasile
Vasile

Reputation: 323

I'm also new to coroutines and my understanding of the coroutine flow is that the code inside the launch will execute like the last execution, right before exiting main function. To put some priorities we use - delay, yield, join. In this example we can change yield with delay and will be the same result.

The flow:

  1. application jump over job = launch get to job.join() and understand that the future code is waiting that job = launch' will finish

  2. application jump over child= launch get to yield() or we can use delay(10) and understand that future code is not important and is going back to the beginning so to child= launch

  3. get to delay(Long.MAX_VALUE) it's a trap

  4. application get to println("Cancelling child") then to child.cancel() and child.join() who is a flow trigger. In this case we can substitute it with yield or join . After this trigger application understand that child = launch is canceled but finally statement is not executed and execute it println("Child is cancelled") .

  5. Execute yield()(i find it useless) then println("Parent is not cancelled") .

    Your Question --- yield() //why do i need this ??????? Because without yield the app will not get back to child= launch will not get inside try block and after when the code will get to child.join(), finally with println("Child is cancelled") will not be executed because try block was not triggered before.

I advice to run this code in debug mode and put breakpoints on every line and use "F9" in Intellij to understand the flow, and also experiment with Alex Yu code from playground and change delay, yield, join

Upvotes: 8

Yuri Schimke
Yuri Schimke

Reputation: 13458

https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/yield.html

Yields a thread (or thread pool) of the current coroutine dispatcher to other coroutines to run. If the coroutine dispatcher does not have its own thread pool (like Dispatchers.Unconfined) then this function does nothing, but checks if the coroutine Job was completed. This suspending function is cancellable. If the Job of the current coroutine is cancelled or completed when this suspending function is invoked or while this function is waiting for dispatching, it resumes with CancellationException.

It accomplishes at least a few things

  1. It temporarily deprioritises the current long running CPU task, giving other tasks a fair opportunity to run.
  2. Checks whether the current job is cancelled, since otherwise in a tight CPU bound loop, the job may not check until the end.
  3. Allows for progress of child jobs, where there is contention because more jobs than threads. This may be important where the current job should adapt based on progress of other jobs.

Upvotes: 31

Related Questions