Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

Question Coroutines which call other coroutines

Discussion in 'Scripting' started by ivoras, Dec 10, 2020.

  1. ivoras

    ivoras

    Joined:
    May 21, 2020
    Posts:
    66
    Let's say I have a wrapper around UnityWebRequest().SendWebRequest, and it's a coroutine itself:

    Code (CSharp):
    1. public static IEnumerator DownloadSomething(string url) {
    2.   var wr = new UnityWebRequest(url);
    3.   yield return wr.SendWebRequest();
    4.   // process errors, handle data, etc.
    5. }
    Do I need to call this wrapper from another coroutine as:

    Code (CSharp):
    1. yield return DownloadSomething("http://example.com/");
    or as

    Code (CSharp):
    1. yield return StartCorutine(DownloadSomething("http://example.com"));
    ... when the goal is to continue processing if any only if the
    DownloadSomething()
    couroutine has finished?

    And what is the actual difference between the two?

    I feel like this topic has been done over a lot of time but I'm still not sure from the posts I've read, especially about the differences between the two approaches.

    Also, does the DownloadSomething() function need to end with a "yield return break;" to indicate it's over or can it just simply end without any special code?
     
  2. MrPaparoz

    MrPaparoz

    Joined:
    Apr 14, 2018
    Posts:
    156
  3. Ray_Sovranti

    Ray_Sovranti

    Joined:
    Oct 28, 2020
    Posts:
    172
    You always need to use StartCoroutine when calling a coroutine. This is because Unity is using IEnumerators in an unusual way. Normally, IEnumerators are used to iterate through a collection of things, which may or may not be a well-defined list; "yield return X" usually returns "the next item in the list to iterate through". (At least, that's my understanding. I haven't actually written IEnumerators outside of Unity coroutines)

    There is no built-in tool in C# that says "run some code, and then wait for the next frame". C# doesn't even have a concept of frames; that's all Unity. So in order for a coroutine to run every frame, you need something to be listening to it, and then re-entering the coroutine each frame. StartCoroutine is how you tell the engine to pay attention to the coroutine.
     
  4. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,531
    In the past nested coroutines had to be seperate coroutines and therefore you had to use StartCoroutine. However since Unity introduced the CustomYieldInstruction class the coroutine scheduler can now run nested IEnumerators within the same coroutine. So to directly answer your question: both ways are possible now.

    An important difference is when just returning the IEnumerator object to the scheduler, no new coroutine will be created but the scheduler will simply iterate the nested IEnumerator and once it's finished it will resume the "outer" IEnumerator. This has several effects. First it reduces the memory footprint slightly as no new seperate coroutine is created. Though the new IEnumerator object of course will still be created. Though the major difference is that when running nested coroutines without StartCoroutine you can stop the outer coroutine using StopCoroutine and it will stop all nested coroutines as well. When you use StartCoroutine the nested coroutine would continue to run as it's a seperate / standalone coroutine and the outer coroutine just waits for the completion of the nested one.

    Note that this blog post is rather old (almost 4 years). I wrote this "coroutine crash course", though I mainly focused on how coroutines work in general. I think it's a bit more detailed than the first chapter of that blog ^^. I recently explained the nesting behaviour over here.
     
    Nixaan likes this.
  5. ivoras

    ivoras

    Joined:
    May 21, 2020
    Posts:
    66
    I thought so, because I have a codebase which uses both and works as expected (well, aside from possible human errors).

    So, by what I understand, there is no way (since the enumerator is returned to the scheduler) for the code following a "yield return MyCouroutine()" to execute before MyCoroutine() finishes? As in, never ever, no edge cases?

    I understand that this way of calling MyCoroutine() executes in the same coroutine as the caller, as far as the scheduler is concerned, and that's ok.
     
  6. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,531
    Well to understand what's happening you have to understand what the scheduler actually does. Keep in mind that a coroutine is not a method but an object representing a statemachine. All the statemachine magic happens by the compiler due to the yield keyword. So from the "user" of such a generated object you simply call MoveNext and it does the next execution up to the next yield. The yielded value can then be read through the Current property, that's all.

    Now the "user" in our case is Unity (or it's scheduler). The scheduler will decide based on what was yielded what to do next. In the past if you just yielded another IEnumerator object, the scheduler would do nothing special and just treat it like any other "unknown" value and just wait for one frame. So the coroutine is rescheduled the next frame. Now since the update when you yield another IEnumerator object, the scheduler will simply remember this IEnumerator (probably on a coroutine local stack) and will simply forward the MoveNext call and the Current value from the IEnumerator at the top of the stack. So the rest will most likely work exactly as it did in the past since now the original IEnumerator will not be touched until the nested one is popped from the stack. This will happen when the IEnumerator object is "done" (that means MoveNext returned false). At this time the "old" IEnumerator will continue. When the stack is empty the whole coroutine is done.

    Note there is a quite old simple coroutine scheduler on the wiki. It works a bit different and isn't really optimised. However as you can see he simply allows the yielding of a simple int or float value and the scheduler decides based on the type if it should schedule this coroutine after x amount of frames (when yielding an int value) or after some amount of time (when yielding a float). That's what Unity could have done as well. However they require you to use a WaitForSeconds instance to indicate waiting for a certain amount of time.

    Unity most likely has seperate lists internally for the different yield results. So when MoveNext returns and the scheduler analyses the yielded value it will probably put the coroutine object into one of many lists. For example there's probably a list of coroutines that should be resumed at the end of the current frame. When a coroutine yields WaitForEndOfFrame, the scheduler recognises the type and simply put the coroutine into that list. When Unity has completed the frame, so somewhere in its internal main loop it will come to the point where those coroutines should resume. So Unity simply iterates through the "wait for end of frame" list and resumes all coroutines in that bucket one by one. Of course the coroutine, unless it's finished, will again yield something. And just like before the scheduler will decide what to do next with the coroutine. In the simple scheduler on the wiki, all coroutines are resumed from Update when their condition is met. However a scheduler may resume a coroutine from whereever necessary. It could even be resumed from another thread which would cause a part of the coroutine to run on a different thread. I think there is an extension packet on the asset store that allows this. So a coroutine could simply yield a special token and the scheduler would resume it on a thread pool. When you need to resume on the main thread they implemented another token to indicate that. The possibilities are endless^^.
     
    Nixaan and seejayjames like this.
  7. ivoras

    ivoras

    Joined:
    May 21, 2020
    Posts:
    66
    Thank you for a very thorough and understrandable answer!
     
  8. yuhuilalala

    yuhuilalala

    Joined:
    Nov 8, 2014
    Posts:
    7
    Code (CSharp):
    1.  
    2. class MB111 : MonoBehavior
    3. {
    4. IEnumerator Do() {
    5. // .. some work.
    6.  
    7. yield return null;
    8. }
    9.  
    10. //// situation I
    11. class MB222 : MonoBehavior
    12. {
    13. MB111 mb111;
    14. IEnumerator Do1() {
    15. // .. some work.
    16.  
    17. yield return mb111.Do();
    18. }
    19.  
    20.  
    21. //// situation II
    22. class MB333 : MonoBehavior
    23. {
    24. MB111 mb111;
    25. IEnumerator Do1() {
    26.  // .. some work.
    27. yield return StartCoroutine(mb111.Do());
    28. }
    29.  

    I encountered another difference:

    • For situation I, errors raise after game object of mb111 destroyed.
    • For situation II, no errors after game object of mb111 destroyed.
    So situation II is more security and recommended.
     
    dimmduh1 likes this.