Wayback Machinekoobas.hobune.stream
May JUN Jul
Previous capture 12 Next capture
2021 2022 2023
1 capture
12 Jun 22 - 12 Jun 22
sparklines
Close Help
  • Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
  • Asset Store
  • Get Unity

UNITY ACCOUNT

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account
  • Blog
  • Forums
  • Answers
  • Evangelists
  • User Groups
  • Beta Program
  • Advisory Panel

Navigation

  • Home
  • Products
  • Solutions
  • Made with Unity
  • Learning
  • Support & Services
  • Community
    • Blog
    • Forums
    • Answers
    • Evangelists
    • User Groups
    • Beta Program
    • Advisory Panel

Unity account

You need a Unity Account to shop in the Online and Asset Stores, participate in the Unity Community and manage your license portfolio. Login Create account

Language

  • Chinese
  • Spanish
  • Japanese
  • Korean
  • Portuguese
  • Ask a question
  • Spaces
    • Default
    • Help Room
    • META
    • Moderators
    • Topics
    • Questions
    • Users
    • Badges
  • Home /
avatar image
7
Question by nevred · May 06, 2016 at 10:56 PM · yieldcoroutinesstopcoroutine

Problem with Stopping Nested Coroutines: Control Never Returned to Parent

Hello!

I am having a problem with stopping a nested Coroutine. It seems that when a parent Coroutine yields to a child and then StopCoroutine() is used to stop the child, that yield never returns and the parent is either permanantly stuck or is just stopped (not sure which). This feels counterintuitive, and I feel like there has to be a solution to this problem, but I can't figure it out at all.

Here is an example class I made to highlight this:

 using UnityEngine;
 using System.Collections;
 
 public class CoroutineTest : MonoBehaviour
 {
     private Coroutine _coLoop;
 
     public void Awake()
     {
         StartCoroutine(CoMain());
         StartCoroutine(CoInterrupt());
     }
 
     public IEnumerator CoMain()
     {
         Debug.Log("Start Main");
         _coLoop = StartCoroutine(CoLoop());
         yield return _coLoop;
         Debug.Log("End Main");
     }
 
     public IEnumerator CoLoop()
     {
         Debug.Log("Start Loop");
         while (Time.time < 2) {
             yield return null;
         }
         Debug.Log("End Loop");
     }
 
     public IEnumerator CoInterrupt()
     {
         yield return new WaitForSeconds(1);
         StopCoroutine(_coLoop);
         Debug.Log("Interrupt Loop!");
     }
 }
 

I would this this to print

Start Main, Start Loop, Interrupt Loop!, End Main

but the return is

Start Main, Start Loop, Interrupt Loop!

and the yield within CoMain() is never returned from, and thus execution stops. I am asking for a solution to this problem without having to use a custom Coroutine implementaton, if possible.

Thank you for any help you can offer. I really appreciate it.

Comment
Add comment · Show 3
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image tanoshimi · May 06, 2016 at 11:13 PM 1
Share

Firstly, + 1 for a well-written question :)

I think this is expected behaviour. Co$$anonymous$$ain()'s execution is paused until CoLoop() yields, and since CoLoop is stopped, this never happens. So Co$$anonymous$$ain() enters an infinite state of waiting for a yield that isn't going to come. CoLoop doesn't yield arbitrarily at the point it is stopped, and I can't find any documentation to suggest why this should happen.

Rather than "solve" this problem (which while being an excellent, clear example, is somewhat artificial), what's the actual scenario in the game you're trying to achieve?

avatar image nevred tanoshimi · May 06, 2016 at 11:34 PM 0
Share

Thank you for your response.

A summary of what I'm trying to do is that I have entities with AI routines attached to them. The routines deter$$anonymous$$e when to move, when to use abilities, etc. If the entity is using an ability, it yields to that ability's routine, which may take a few seconds to end, depending on a few factors within the game. If the entity's use of that ability is interrupted before the ability finishes, the parent AI routine stops execution, and the entity just sits idle for the rest of play.

A heavily abstracted example:

     IEnumerator AIRoutine()
     {
         while (!canCast) {
             $$anonymous$$ove();
             yield return null;
         }
 
         yield return ability.Cast();
     }
 
     class Ability {
         IEnumerator Cast()
         {
             yield return new WaitForSeconds(castTime);
             //do ability stuff
         }
 
         void Interrupt()
         {
             StopAllCoroutines();
         }
     }

When Interrupt() is called on an Ability, AIRoutine() stops executing. If this is intended behavior, then how can what I want be accomplished? Is looping over yield return null; and checking if the Ability should be interrupted rather than WaitForSeconds() a better solution? $$anonymous$$aybe I'm wrong, but that feels really expensive if you have dozens or even hundreds of Ability routines running at once, no?

avatar image Bunny83 · May 07, 2016 at 04:52 AM 0
Share

Related question:

http://answers.unity3d.com/questions/324469/coroutine-does-not-stop-when-having-a-coroutine-in.html

3 Replies

· Add your reply
  • Sort: 
avatar image
10
Best Answer

Answer by Bunny83 · May 07, 2016 at 05:40 AM

First of all, coroutines fundamentally run independent from each other. When you call StartCoroutine Unity registers a new coroutine which will take it's place in some internal list. It you "yield" on another Coroutine, that coroutine is basically on hold until the "nested" coroutine finishes. It's possible that they implemented the continuation with some sort of callback which gets invoked inside the coroutine scheduler when the nested coroutine finishes. A coroutine is finished when MoveNext returns false.

However stopping the coroutine completely breaks this mechanism since the coroutine is simply kicked out of the internal list. So the "outer" coroutine still waits for it's signal to continue which will never be send.

As i said in a comment on the other question, stopping a coroutine should in general be avoided. While it's a lot saver than terminating a thread from the outside (since a coroutine can only be stopped at yields) it's still a bit unpredictable in which state the coroutine is at the moment. You should implement the termination into the nested coroutine and let it finish gracefully (i.e. yield break;)

One option would be to run the nested coroutine "inline" without actually starting a new coroutine.

  bool terminateNested = false;
  public IEnumerator CoMain()
  {
      Debug.Log("Start Main");
      terminateNested = false;
      IEnumerator nested = CoLoop(); // no StartCoroutine!!!
      while(!terminateNested && nested.MoveNext())
          yield return nested.Current;
      Debug.Log("End Main");
  }

Here we iterate the IEnumerator manually. We simply yield the yielded value of the nested coroutine, so it has the same effect. You can simply set "terminateNested" to "true" and the nested coroutine will "terminate". However the last yield will still complete. So if the nested coroutine yields a "WaitForSeconds(1000)" CoMain can not continue until that wait is done.

Another solution is to build a wrapper around the IEnumerator of the nested coroutine and provide a seperate way of stopping the coroutine. The wrapper would simply return another controlling coroutine which you actually yield on in your CoMain. When we receive the terminate signal we stop the "wrapped" coroutine and let the controlling coroutine terminate gracefully.

Something like that:

 public class StoppableCoroutine
 {
     bool terminated = false;
     IEnumerator payload;
     Coroutine nested;
     MonoBehaviour mb;
     public StoppableCoroutine(MonoBehaviour mb, IEnumerator aCoroutine)
     {
         payload = aCoroutine;
         nested = mb.StartCoroutine(wrapper());
         this.mb = mb;
     }
     public Coroutine WaitFor()
     {
         return mb.StartCoroutine(wait());
     }
     public void Stop()
     {
         terminated = true;
         mb.StopCoroutine(nested);
     }
     private IEnumerator wrapper()
     {
         while (payload.MoveNext())
             yield return payload.Current;
         terminated = true;
     }
     private IEnumerator wait()
     {
         while(!terminated)
             yield return null;
     }
 }
 
 public statoc class MonoBehaviourExtension
 {
     public static StoppableCoroutine StartCoroutineEx(this MonoBehaviour mb, IEnumerator coroutine)
     {
         return new StoppableCoroutine(mb, coroutine);
     }
 
 }


With that class and extension method you can simply do this:

  StoppableCoroutine _coLoop;
  
  public IEnumerator CoMain()
  {
      Debug.Log("Start Main");
      _coLoop = StartCoroutineEx(CoLoop());
      yield return _coLoop.WaitFor();
      Debug.Log("End Main");
  }

To actually stop the nested coroutine you would simply call

  _coLoop.Stop();

This approach has several advantages. Now you can have multiple coroutines to wait for a single one which isn't possible with the Coroutine object. You can only yield a Coroutine object once. Since our WaitFor method creates a new coroutine each time you call it that problem doesn't exist.

Finally a link to my CoroutineHelper class which does something similar.

Comment
Add comment · Show 5 · Share
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image nevred · May 07, 2016 at 08:13 AM 1
Share

Not using StopCoroutine() anymore makes sense. Thank you. It seems strange that the outer Coroutine can be put in the position of waiting for a signal it will never get. Does it sit in the Coroutine scheduler indefinitely? Is that a memory use concern?

What I ended up doing is I replaced WaitForSeconds() with something similar to this:

             float endTime = Time.time + timeToWait;
 
             while (Time.time < endTime) {
                 if (interrupt) {
                     yield break;
                 }
 
                 yield return null;
             }

I think this is functionally pretty similar to your example, but I think I like yours better. Thanks again!

avatar image Bunny83 nevred · May 09, 2016 at 10:29 AM 0
Share

Yes, the coroutine would most likely sit in the scheduler and keep the memory of all related classes alive. It should go away when you call StopAllCoroutines or when you destroy the object the coroutine runs on.

avatar image tsibiski · Oct 04, 2016 at 08:40 PM 1
Share

Bunny, your work here is fantastic! It has made my life a lot easier.

I wanted to point out a bug I found in it.

 nested = mb.StartCoroutine(wrapper());

That line of code should be in "WaitFor()| rather than the constructor. The reason is that if someone were to call this line of code...

 _currentTest$$anonymous$$ethod = $$anonymous$$onoBehaviourExtension.StartCoroutineEx(instance, _currentTestEnumerator);
 

... the variable "_currentTest$$anonymous$$ethod" will not actually be set until "mb.StartCoroutine(wrapper());" is complete.

The use case where this caused an issue for me is in a test automation framework. One test after another is being launched. When an assertion fails, a property retrieves "_currentTest$$anonymous$$ethod" and "Stop()" is called on this object. However, since the test method is not complete, the constructor has still not returned the constructed StoppableCoroutine, so the "_currentTest$$anonymous$$ethod" variable is still set to the previous test's StoppableCoroutine.

Since that method is long gone, it tries to stop an already destroyed object. This throws an error, of course. It is not a fatal error, but it took some time to understand the root cause.

After I move that line of code, everything works as I expect it to.

avatar image Bunny83 tsibiski · Oct 05, 2016 at 01:10 AM 0
Share

Well, that's a very specific case and yes it will cause a bug, but it's not a bug in this class since the behaviour is pretty clear. The problem is that Unity immediately runs the coroutine up to the first yield. One simply solution is to place a simple "yield return null;" right at the start of the coroutine to make it wait one frame. Another way would be to use a second internal coroutine which will add that extra yield before the actual coroutine is started.

You shouldn't start the actual coroutine in "WaitFor" as WaitFor is designed in a way so it can be used in multiple coroutines. If you want to defer the launch of the coroutine you can add a Start method (just like the Thread class has one). That would be a cleaner solution.

avatar image kolboch · Nov 08, 2018 at 11:08 PM 0
Share

I'm not sure if there is a chance to still get some answer from you but what pros and cons are in stopping parent coroutine in such scenario? Would you recommend it to prevent leaks?

ex. StartCorotouine(ParentIEnum = ParentCoroutine())

and later if(ParentIEnum != null) { StopCoroutine(ParentIEnum); ParentIEnum = null; }

avatar image
0

Answer by Varaughe · Oct 08, 2018 at 10:46 AM

This actually is a Unity drama, stopping a coroutine without yielding nothing and blocking the "yield return StartCoroutine". A solution would be, to never use "yield return StartCoroutine(aCoroutine)" if that aCoroutine is to be stopped with StopCoroutine() at some point, rather simply StartCoroutine(aCoroutine), and after that "yield return null" until that aCoroutine gets null:

 IEnumerator coroutineMethod;
 
 void Start(){
     StartCoroutine(MainCoroutine());
 }
 
 void Update(){
       if(conditionToStopCoroutine){
            conditionToStopCoroutine = false;
 
            if(null != coroutineMethod){
                 StopCoroutine(coroutineMethod);
                 coroutineMethod = null;
            }
       }
 
 }
 
 IEnumerator MainCoroutine(){
 
    coroutineMethod = CoroutineMethod();
 
    StartCoroutine(coroutineMethod);
    while(coroutineMethod  != null){
         yield return null;
    } 
    //so the code that follows this should be executed propely
     Debug.LogError("Sipvay Barmane!!!");
 }
 
 IEnumerator CoroutineMethod(){
     // ..
    // ..
    // ..
 
   //last statement:
    coroutineMethod = null;
 }











Comment
Add comment · Share
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users
avatar image
0

Answer by GarlicDipping · Dec 15, 2018 at 10:09 AM

This is quite an old thread but writing this answer here because Bunny83's code gave me some insight!

So, I wanted a more flexible nestable coroutine so that extended @Bunny83 's class with IEnumerator.

Code :

 public class StoppableCoroutine
 {
     MonoBehaviour monoParent;
     Coroutine wrapper;
     NestableCoroutine nestableCoroutine;
     bool terminated = false;
     public StoppableCoroutine(MonoBehaviour monoParent, IEnumerator initialCoroutine)
     {
         this.monoParent = monoParent;
         nestableCoroutine = new NestableCoroutine(initialCoroutine);
         wrapper = monoParent.StartCoroutine(Run());
     }
 
     public void Stop()
     {
         terminated = true;
         monoParent.StopCoroutine(wrapper);
     }
 
     public IEnumerator StartNested(IEnumerator newCoroutine)
     {
         nestableCoroutine.Add(newCoroutine);
         while(nestableCoroutine.IsCoroutineDone(newCoroutine) == false)
         {
             yield return null;
         }
     }
     
     private IEnumerator Run()
     {
         //Wait a frame so that user can safely access this instance inside 'initialCoroutine'
         //JUST AFTER constructor returns.
         yield return null;
         while (!terminated && nestableCoroutine != null && nestableCoroutine.MoveNext())
             yield return nestableCoroutine.Current;
     }
 
 
     internal class NestableCoroutine : IEnumerator
     {
         List<IEnumerator> all;
         IEnumerator latestCoroutine
         {
             get
             {
                 if(all == null || all.Count == 0)
                 {
                     return null;
                 }
                 else
                 {
                     return all[all.Count - 1];
                 }
             }
         }
         public NestableCoroutine(IEnumerator first)
         {
             all = new List<IEnumerator>();
             all.Add(first);
         }
 
         public void Add(IEnumerator recent)
         {
             all.Add(recent);
         }
 
         public bool IsCoroutineDone(IEnumerator coroutineToCheck)
         {
             return all.Contains(coroutineToCheck);
         }
 
         public object Current
         {
             get
             {
                 if (latestCoroutine == null)
                 {
                     return null;
                 }
                 else
                 {
                     return latestCoroutine.Current;
                 }
             }
         }
 
         public bool MoveNext()
         {
             if (latestCoroutine == null)
             {
                 return false;
             }
             else
             {
                 bool canMoveNext = latestCoroutine.MoveNext();
                 if (canMoveNext == false)
                 {
                     all.RemoveAt(all.Count - 1);
                     if(all.Count == 0)
                     {
                         return false;
                     }
                     else
                     {
                         return true;
                     }
                 }
                 else
                 {
                     return canMoveNext;
                 }
             }
         }
 
         public void Reset()
         {
         }
     }
 }
 

With this you can nest coroutine at any depth as you want, and there's no need to keep track of every StoppableCoroutine class instances - Only initial one.

Sample Code:

 using System.Collections;
 using System.Collections.Generic;
 using UnityEngine;
 
 public class Test_StoppableCoroutine : MonoBehaviour
 {
     StoppableCoroutine stoppableCoroutine;
     // Update is called once per frame
     void Update ()
     {
         if(Input.GetKeyDown(KeyCode.S))
         {
             stoppableCoroutine = this.StartStoppableCoroutine(Outer());
         }
         else if (Input.GetKeyDown(KeyCode.Q))
         {
             Debug.Log(Time.time + " / Q Pressed!");
             stoppableCoroutine.Stop();
         }
     }
 
     IEnumerator Outer()
     {
         Debug.Log(Time.time + " / Start Outer");
         yield return stoppableCoroutine.StartNested(Inner());
         Debug.Log(Time.time + " / End Outer");
     }
 
     IEnumerator Inner()
     {
         int count = 0;
         while (count < 5)
         {
             Debug.Log("Inner running...");
             yield return new WaitForSeconds(0.5f);
             count++;
         }
         yield return stoppableCoroutine.StartNested(InnerInner());
     }
 
     IEnumerator InnerInner()
     {
         while (true)
         {
             Debug.Log("InnerInner running...");
             yield return new WaitForSeconds(0.5f);
         }
     }
 }
 

If there's any mistakes or caveats I missed, Please let me know!

Comment
Add comment · Share
10 |3000 characters needed characters left characters exceeded
▼
  • Viewable by all users
  • Viewable by moderators
  • Viewable by moderators and the original poster
  • Advanced visibility
Viewable by all users

Your answer

Hint: You can notify a user about this post by typing @username

Up to 2 attachments (including images) can be used with a maximum of 524.3 kB each and 1.0 MB total.

Follow this Question

Answers Answers and Comments

9 People are following this question.

avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image avatar image

Related Questions

Inconsistent StopCoroutine behavior with CustomYieldInstruction 1 Answer

how to restart coroutine 2 Answers

Is there any way to stop WaitForSeconds() ? 1 Answer

Does a coroutine end itself automatically? 2 Answers

Trouble Resuming after Yielding while Inside Coroutine 1 Answer


Enterprise
Social Q&A

Social
Subscribe on YouTube social-youtube Follow on LinkedIn social-linkedin Follow on Twitter social-twitter Follow on Facebook social-facebook Follow on Instagram social-instagram

Footer

  • Purchase
    • Products
    • Subscription
    • Asset Store
    • Unity Gear
    • Resellers
  • Education
    • Students
    • Educators
    • Certification
    • Learn
    • Center of Excellence
  • Download
    • Unity
    • Beta Program
  • Unity Labs
    • Labs
    • Publications
  • Resources
    • Learn platform
    • Community
    • Documentation
    • Unity QA
    • FAQ
    • Services Status
    • Connect
  • About Unity
    • About Us
    • Blog
    • Events
    • Careers
    • Contact
    • Press
    • Partners
    • Affiliates
    • Security
Copyright © 2020 Unity Technologies
  • Legal
  • Privacy Policy
  • Cookies
  • Do Not Sell My Personal Information
  • Cookies Settings
"Unity", Unity logos, and other Unity trademarks are trademarks or registered trademarks of Unity Technologies or its affiliates in the U.S. and elsewhere (more info here). Other names or brands are trademarks of their respective owners.
  • Anonymous
  • Sign in
  • Create
  • Ask a question
  • Spaces
  • Default
  • Help Room
  • META
  • Moderators
  • Explore
  • Topics
  • Questions
  • Users
  • Badges