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

How to stop a coroutine that has NESTED inner coroutines?

Discussion in 'Scripting' started by ez06, Jul 4, 2020.

  1. ez06

    ez06

    Joined:
    Feb 18, 2014
    Posts:
    45
    I have this coroutine:

    Code (CSharp):
    1. IEnumerator MainCoroutine() {
    2.       while (true) {
    3.             yield return StartCoroutine(myCoroutine1());
    4.             yield return StartCoroutine(myCoroutine2());
    5.       }
    6. }
    Basically I do: myMainCoroutine = StartCoroutine(MainCoroutine());

    As shown in the code, myCoroutine1 is executed first, and myCoroutine2 starts once myCoroutine1 is done.

    The problem is: if I do StopCoroutine(myMainCoroutine), the coroutines 1 and 2 are still called.

    How can I make sure all those inner coroutines are stopped along with the "parent " coroutine?
     
  2. raarc

    raarc

    Joined:
    Jun 15, 2020
    Posts:
    535
    you need to do

    Code (CSharp):
    1. Coroutine cr =StartCoroutine(MainCoroutine());
    2.  
    3. StopCoroutine(cr);
    4.  
     
  3. Elango

    Elango

    Joined:
    Jan 27, 2016
    Posts:
    107
    If StopAllCoroutines is not an option then:
    Code (CSharp):
    1.     IEnumerator MainCoroutine()
    2.     {
    3.         while (true)
    4.         {
    5.             yield return myCoroutine1();
    6.             yield return myCoroutine2();
    7.         }
    8.     }
     
    Bunny83, efimovrusl and Fenikkel like this.
  4. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,923
    I don't think there's any ownership of coroutines, so there's no way for Unity to know Maincoroutine owns that instance of myCoroutine1.

    Make another global coroutine variable, maybe "mySubcoroutine". Add "mySubcoutine=" before every StartCoroutine. Then call StopCoroutine on them both. This only works if you have a single sub-coroutine at a time. If not, you get to make a list which your main coroutine maintains (I've done that, it seemed to work, but it wasn't pretty).
     
  5. cielbird

    cielbird

    Joined:
    Dec 15, 2017
    Posts:
    11
    This should work:
    Code (CSharp):
    1. IEnumerator MainCoroutine() {
    2.       while (true) {
    3.             yield return myCoroutine1();
    4.             yield return myCoroutine2();
    5.       }
    6. }
    Why? Calling `StopCoroutine` only stops the coroutine you started with `StartCoroutine`. The other coroutines are still running!
     
    Bunny83 and Fenikkel like this.
  6. MDADigital

    MDADigital

    Joined:
    Apr 18, 2020
    Posts:
    2,198
    Instead of start a new coroutine execute and foreach yield return the inner one
     
  7. Owen-Reynolds

    Owen-Reynolds

    Joined:
    Feb 15, 2012
    Posts:
    1,923
    The problem is you're always going to have your main coroutine, M, waiting on at least one other running "sub"-coroutine. When you stop M, sub-coroutine A, B or C will still run. Somehow you have to remember "coroutine(s) M is running" and also stop those.

    It's a real problem. Imagine M is pulling parts into place 1 at a time. It you stop it since the object just got blasted, you'll have one part still being pulled into where it should be (and also getting tossed, so it will look weird and run for a long time).

    It's worse since M could be running C which could be running coroutine X. For example the "pull into place" coroutine might run an existing "rotate to correct orientation" coroutine.before it starts moving. We stop M and C, but the stupid object is trying to rotate.

    One solution, which I think worked, was a list of sub-coroutines (created when you called a "main" coroutine) When you started a sub-coroutine, it went onto the list, then off when it stopped. Each sub-coroutine was given the list to add it's own sub-coroutines to. "Stop coroutine" was a function to also stop everything in the list. It was a huge pain. You could only call other coroutines which used the list or never called other coroutines. I think my next solution was to give every coroutine a magic "am I done" object. Every coroutine loop needed to check this, and also after every waitForSeconds. It has a worse problem that you can't call a "normal" coroutine, since it won't know to stop. It's also buggier since it's easy to forget to check in each spot.
     
  8. GixG17

    GixG17

    Joined:
    Aug 18, 2013
    Posts:
    105
    Something like this should satisfy your needs:

    1. Code (CSharp):
      1. List<IEnumerator> myCoroutines;
      2.  
      3. void Start()
      4. {
      5.      if (this.myCoroutines != null) {
      6.           StopMyCoroutines();
      7.      }
      8.      this.myCoroutines = new List<IEnumerator>(3);
      9.      this.myCoroutines.Add(MainCoroutine());
      10.      StartCoroutine(this.myCoroutines[0]);
      11. }
      12.  
      13. IEnumerator MainCoroutine() {
      14.      this.myCoroutines.Add(myCoroutine1());
      15.      yield return StartCoroutine(this.myCoroutines[1]);
      16.      this.myCoroutines.Add(myCoroutine2());
      17.      yield return StartCoroutine(this.myCoroutines[2]);
      18. }
      19.  
      20. void StopMyCoroutines()
      21. {
      22.      foreach (IEnumerator cor in this.myCoroutines)
      23.      {
      24.           StopCoroutine(cor);
      25.      }
      26. }
    I hope this helps.
     
  9. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,531
    That's all not necessary if you do what @cielbird suggested. The point here is Unity now supports running IEnumerators as sub routines without starting a new coroutine. The sub IEnumerator is simply run in place. So in essence when you do

    Code (CSharp):
    1.     IEnumerator MainCoroutine() {
    2.           while (true) {
    3.                 yield return myCoroutine1();
    4.                 yield return myCoroutine2();
    5.           }
    6.     }
    7.  
    What Unity does behind the scenes is this:

    Code (CSharp):
    1.     IEnumerator MainCoroutine() {
    2.           while (true) {
    3.                 var tmp = myCoroutine1();
    4.                 while (tmp.MoveNext())
    5.                     yield return tmp.Current;
    6.                 var tmp2 = myCoroutine2();
    7.                 while (tmp2.MoveNext())
    8.                     yield return tmp2.Current;
    9.           }
    10.     }
    11.  
    As you can see, when you simply yield an IEnumerator, inside a coroutine, Unity does not start a new coroutine but simply iterates the nested enumerator in place. Unity probably uses a stack internally inside the coroutine on the native side. Usually one coroutine runs one IEnumerator. However when you yield another IEnumerator it's pushed onto the stack and this is run instead. When the nested IEnumerator has finished it simply pops it from the stack and continues with the outer IEnumerator. Once the stack is empty the whole coroutine is finished. So when stopping the coroutine you simply stop the whole hierarchy.

    Of course when you actually start a new standalone coroutine using StartCoroutine, that new coroutine runs independently from the outer coroutine. The outer coroutines simply waits for the completion of the other coroutine. Stopping the outer coroutine would not stop the nested one.

    Note: I generally advice to avoid Stopping coroutines from the outside. Even though coroutines are more reliable and controlled compared to threads, the reasons why stopping them from the outside are quire similar. The outside code that is stopping the coroutine or thread, does not necessarily know in which state the coroutine / thread is and could cause all sorts of race conditions or unexpected behaviour that is extremely difficult to reason about.

    Finally another word of caution: Unity has a bug for ages (and I just checked that it still exists in Unity 2021.1.24f1): When you yield on another coroutine inside an outer coroutine, stopping that inner coroutine would not cause the outer coroutine to die silently. Stopping a coroutine would essentially eliminate it / kick it out of the scheduler. This means another coroutine that is waiting for the completion of that nested coroutine will get kicked out as well and never continues. For example, imagine this setup:

    Code (CSharp):
    1. Coroutine nested;
    2.  
    3. IEnumerator MainCo()
    4. {
    5.     Debug.Log("Main started");
    6.     nested = StartCoroutine(NestedCo());
    7.     Debug.Log("Main finished");
    8. }
    9.  
    10. IEnumerator NestedCo()
    11. {
    12.     for(int i = 0; i < 100; i++)
    13.         yield return new WaitForSeconds(1);
    14. }
    15.  
    16. StartCoroutine(MainCo());
    17.  
    Now when you use
    StopCoroutine(nested);
    while those two coroutines are running, the MainCo will not continue and never prints "Main finished". Of course if the nested coroutine is not stopped but simply finished after 100 iteration it would print "Main finished".

    By implementing the outer IEnumerator ourselfs we can implement a destructor / finalizer to detect when the IEnumerator instance is garbage collected. When we stop the inner coroutine, we can see that the outer coroutine simply vanishes. This behaviour is another reason why you should avoid stopping coroutines from the outside whenever possible.

    edit

    ps: if someone is looking for a more in-depth explanation how coroutines work behind the scenes, I've written this coroutine crash course on UnityAnswers.
     
    Last edited: Jan 10, 2022