Search Unity

Proper way to stop coroutine?

Discussion in 'Scripting' started by baxcet20, Jul 2, 2019.

  1. baxcet20

    baxcet20

    Joined:
    Jul 1, 2019
    Posts:
    28
    I would want to utilize IEnumerator in my game, but I don't want them to waste processing cycles and fill my memory with trash, so I was wondering whether this was a proper way to handle this. Note that I want it to do it's task and then be purged, it's functionality has been rushed, I'm more interested in termination. Trigger, perform and be disposed is the thought:

    Code (CSharp):
    1. IEnumerator MyFunction() {
    2.     for (float f = 1f; f >= 0; f -= 0.1f) {
    3.         Color c = GetComponent<Renderer>().material.color;
    4.         c.a = f;
    5.         GetComponent<Renderer>().material.color = c;
    6.  
    7.         if (f < 0) {
    8.             StopCoroutine("MyFunction");
    9.         }
    10.         yield return null;
    11.     }
    12.  
    13. }
     
  2. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    They work like regular methods. Once they reach the end of their sequence, they are disposed from the stack.
    No need to explicitly call
    StopCoroutine()
    .
     
    Bunny83 likes this.
  3. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,735
    Line 8 should simply be:

    Code (csharp):
    1. yield break;
    It is the coroutine equivalent of:

    Code (csharp):
    1. return;
     
  4. baxcet20

    baxcet20

    Joined:
    Jul 1, 2019
    Posts:
    28
    Don't coroutines repeat themself?
    If so, how does computer know what I'm trying to achieve? What if I want to go from 0.5 to -0.5. How does computer not kill the coroutine at 0.0?
     
  5. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    Coroutines simply allow you to pause code execution between frames.
    You still have to use a for/foreach/while/do-while loop to apply any looping sequence, and with all loops, they stop when their condition is met.

    This will not loop:
    Code (CSharp):
    1. IEnumerator Example() {
    2.    Debug.Log("Waiting...");
    3.    yield return new WaitForSeconds(3f);
    4.    Debug.Log("Done.");
    5. }
    This will loop:
    Code (CSharp):
    1. IEnumerator Example() {
    2.    WaitForSeconds wait = new WaitForSeconds(1f);
    3.    Debug.Log("Counting...");
    4.  
    5.    for(int i = 1; i <= 10; i++) {
    6.       Debug.Log(i);
    7.       yield return wait;
    8.    }
    9.  
    10.    Debug.Log("Done.");
    11. }
    In both examples, the Coroutines are disposed once they're finished executing.
     
    bruno_agonalea likes this.
  6. baxcet20

    baxcet20

    Joined:
    Jul 1, 2019
    Posts:
    28
    Ah, so it's like an independent thread. Doing it's all stuff from top to bottom, and will end itself when it runs out like a regular function call?
     
  7. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    Pretty much. It's supposed to be Unity's single-threaded approach to multi-threading, as ironic as that sounds.
     
  8. baxcet20

    baxcet20

    Joined:
    Jul 1, 2019
    Posts:
    28
    That makes very little sense from my point of view, why use this and not InvokeRepeating, could you possibly know an example off top of your head?
     
  9. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,735
    Coroutines are an IEnumerator object. They have their own internal state. It's different from just calling InvokeRepeating(), which would restart a fresh function call each time.

    Here is ALL you need to know about coroutines in Unity:

    1. they are an IEnumerator, also known in some languages as a Generator. It is an object with a .MoveNext() method on it that "yields" the next thing with each subsequent call. That is what you create by calling an IEnumerator: you create the object, you do NOTHING with it, i.e., NONE of your code is actually executed.

    2. calling StartCoroutine() with that object causes Unity to IMMEDIATELY call the .MoveNext() method on that object, which runs until the first yield, which returns something. Unity does this first step BEFORE returning from StartCoroutine().

    3. By Unity's convention based on what your object returns each time, Unity decides whether and when to ever call .MoveNext() again: wait a bit, wait one frame, never call again, etc.

    That's it. Done. There is literally NOTHING more to coroutines than that.
     
    erkamuzuncayir, paladim and PutridEx like this.
  10. StarManta

    StarManta

    Joined:
    Oct 23, 2006
    Posts:
    8,775
    They do very different things, unless you have coded your coroutine just so to make it do exactly the same thing that InvokeRepeating does. Broadly speaking, coroutines are much, much more powerful and versatile than InvokeRepeating.
    If you want to do a detailed sequence of different code (Move forward, turn left, turn on the light, move backward), coroutines can do that, InvokeRepeating can't.
    If you want code that runs on every frame for a certain amount of time, coroutines can do that, InvokeRepeating can't.
    If you want the code to wait until some condition is met before running code, coroutines can do that, InvokeRepeating can't.
    I could go on for literally pages about the things that coroutines can do but InvokeRepeating can't.

    As far as this description:
    That is absolutely apt. In proper multithreading, the threaded process will get shunted off to the OS to let it decide how to most efficiently juggle multiple things at once, which can include spreading the threads across multiple processor cores.
    With coroutines, Unity is still running everything in the same thread, but it will put coroutines on hold (or rather, the coroutines put themselves on hold, which is what yield return means) while it executes other things in the game loop. There's zero chance that your Update function will be running at the same time as code in your coroutine, but the game engine will jump back and forth between the two on a frame by frame basis*, giving the illusion of threaded code. Does that make more sense?

    * Unless you yield return something more specific, like WaitForSeconds
     
  11. Vryken

    Vryken

    Joined:
    Jan 23, 2018
    Posts:
    2,106
    InvokeRepeating() can only specify an amount of seconds to repeat, whereas Coroutines have a variety of yield instructions, those being:
    Unlike Coroutines, which dispose themselves after they finish executing, any methods called from InvokeRepeating will never stop executing until CancelInvoke() is called. If this is what you want to happen, then you can totally use InvokeRepeating over a Coroutine. It just depends on what you want to accomplish.
     
  12. Ziplock9000

    Ziplock9000

    Joined:
    Jan 26, 2016
    Posts:
    360
    Just a quick note for anyone looking. A coroutine method is fundamentally different to a thread, so don't think of them in the same way. It uses a fairly crude manual time-slicing method to return control back to unity for a time in a loop.
    Crue does not mean bad, however, as they also avoid some of the very complicated issues that come with threads too.
     
  13. ironCookie

    ironCookie

    Joined:
    Sep 3, 2014
    Posts:
    10
    Hi everyone.

    First of all, I wanna say thanks, sharing all these infos about coroutines.

    The original problem should already be solved by now.
    Nevertheless, I still want to state out one question which I couldn't answer for quite some time in the past.

    For a long time I was wondering, where exactly a couroutine ends when calling StopCoroutine. At any statement inside a coroutine or at the next yield statement? By reading the answer of @StarManta it becomes more clearly. Moreover I do have found this: https://answers.unity.com/questions/308830/what-will-stopcoroutine-exactly-do.html.

    To answer my own question:
    As long as StopCouroutine is not called from a custom thread, all statements inside a couroutine will be executed until the next yield statement is reached.

    I hope that will help others, looking for an answer to the same question.
     
  14. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    Thank you! This critically important and unexpected information is - of course! - in 2022 still missing from the Unity API docs. Unity does NOT do what the API says (StopCoroutine actually does: RequestStopCoroutine, or ScheduleStopCoroutine).

    Took ages to figure out a bug caused by this crappy behaviour of Unity (and even crappier documentation). I knew it from previous trial and error, but it had been so long since I was debugging Unity coroutines I'd forgotten it (and when I checked the docs - they say nothing).
     
  15. cryy22

    cryy22

    Joined:
    Jul 8, 2021
    Posts:
    3
    your experience here is useful for reminding everyone that coroutines are completely synchronous. the Unity execution order will not advance, no input callbacks will be called, no Update loop code will be run, no OnDestroy hooks will be called, etc etc etc, until a Coroutine hits its yield statement. everybody has to wait for the coroutine to hit a yield, the same way everyone has to wait for each MonoBehaviour's Update callback to return, one at a time, on the main thread.

    Coroutines become really easy to reason through once you understand that they are simple. they are fully deterministic and you can easily determine which code was run, in which order, in any given frame.
     
    Kurt-Dekker likes this.
  16. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    Well, OK, but ... don't be fooled into believing Coroutines are as simple as 'completely synchronous' -- the behaviour for invocation is poorly defined / documented.

    They are, at best: "synchronous ... after at least 2 frames have elapsed". Edge cases! Beware! Undocumented behaviours!

    e.g. the main one that trips me up once every couple of years: When you start a coroutine, or yield to it, does it 'run once immediately, then yield', or does it 'wait until the next frame, then run once until it yields', or does it "run at the end of the frame, after the current code has completed, then run until it yields?'. Easy to get muddled between those.
     
  17. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    Eh, no, none of this, they're just completely synchronous.

    When you start a coroutine it runs immediately and synchronously up until the first
    yield
    statement it encounters. At that point it yields control back to the caller.
     
    cryy22 likes this.
  18. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    Eh, shrug. If you've never hit the darker corners of this - good for you. But there's some interesting subtleties behind the scenes, off the top of my head the fun bits are:

    1. Which is your first yield instruction? Not always what it seems (bear in mind they can get inserted implicitly, as well as the explicit ones you wrote into your source code)
    2. What does it mean to "start" a coroutine? Which API call are you using to do that? What was the runtime context of that API invocation (outcome is not always what it seems - it's NOT "run immediately" in all cases, depending on when/where/how that call was processed).
    3. When does the coroutine get scheduled? It's not necessarily 'this frame', in some cases it is supposed to be 'after this frame', based on when in the frame the invocation happened.
     
  19. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    The first one the code execution encounters, always. You can easily identify them because the line starts with
    yield
    . Unity never inserts any "implicit yields" in your coroutine.

    With StartCoroutine of course. The initial execution will never be delayed, regardless of where you started it. The only other way to start a coroutine is to yield the IEnumerator directly from another coroutine. That would give you the one "dark corner" situation I believe in this whole thing. https://forum.unity.com/threads/dif...ld-return-startcoroutine.432571/#post-2797412 . This "dark corner" can of course be avoided by simply always starting your coroutines with StartCoroutine.

    It begins running immediately when you call StartCoroutine, and runs until it reaches the first yield (or of course, the end of the method, or if an exception is thrown). In fact it basically works exactly like any other "normal" method call until reaching the first yield.

    I have run into all the "dark corners" of coroutines that can be run into in my time in Unity, and found that they're actually much simpler than most people seem to think.
     
    Last edited: Feb 2, 2023
  20. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,735
    I won't disagree that some of my most perplexing earliest bugs with Unity were related to misunderstanding coroutines, and judging from all other posts I've seen I was not alone.

    However, Coroutines really are super-super-super simple.

    As Praetor posts, MonoBehaviour.StartCoroutine() is almost guaranteed the way 99.99% of people who are "starting coroutines" are going to use.

    Can you run coroutines other ways? OF COURSE! But then you wouldn't ask about that in a Unity forum because it doesn't involve Unity. That's on you. Let's just assume everybody posting here is using StartCoroutine()

    That means:

    - it WILL run until the first yield before StartCoroutine() even returns once (kinda like .Awake() WILL run before .AddComponent<T>() returns)

    - there are only the yields you see in your code, no others (this is synchronous code, honest!)

    - And every other time Unity runs it is based on Unity's notion of time having advanced far enough, OR, more simply if you only
    yield return null;
    then it will be immediately eligible again to receive the next .MoveNext() call in the normal game loop.

    But ... but ... I hear you asking,... "What is this normal game loop you speak of?!"

    Why I'm glad you asked! Here's my favorite Unity diagram of all times:

    https://docs.unity3d.com/Manual/ExecutionOrder.html

    Finally, just for completness:

    Coroutines in a nutshell:

    https://forum.unity.com/threads/des...en-restarting-the-scene.1044247/#post-6758470

    https://forum.unity.com/threads/proper-way-to-stop-coroutine.704210/#post-4707821

    Splitting up larger tasks in coroutines:

    https://forum.unity.com/threads/bes...ion-so-its-non-blocking.1108901/#post-7135529

    Coroutines are NOT always an appropriate solution: know when to use them!

    https://forum.unity.com/threads/things-to-check-before-starting-a-coroutine.1177055/#post-7538972

    https://forum.unity.com/threads/heartbeat-courutine-fx.1146062/#post-7358312

    Our very own Bunny83 has also provided a Coroutine Crash Course:

    https://answers.unity.com/questions...ected.html?childToView=1749714#answer-1749714
     
  21. a436t4ataf

    a436t4ataf

    Joined:
    May 19, 2013
    Posts:
    1,933
    As the diagram implies: it matters when / where you invoke StartCoroutine - results are not identical. Try invoking inside LateUpdate, for instance. Or inside a callback that was triggered as a side-effect of a stage-change in the Unity Animation system.

    I've written two games with fairly large numbers of coroutines (typically chaining many tens of coroutines at once, across seconds of real time, and doing parallel execution as well as serial execution), and submitted bug reports to Unity for some of the weirdness that happens - especially when you get into nesting multiple levels of coroutine. When it works, it's awesome and everything is fantastic. When it goes wrong you get some 1-frame hiccups where things didn't quite start where you expected them to - and usually that's fine, but sometimes its not. There are bugs in your own code that are easy to miss (I've done that a lot along the way) and there are features Unity never implemented properly (like stack traces - maybe that's fixed in 2021 finally? Last time I checked it still seemed broken - if you want stack traces on coroutines you have to implement them yourself).

    The docs are still missing some of the basics of a coroutine - e.g. I see nothing on how to launch multiple in parallel and do a metaphorical Join() on them completing - while going into a lot of detail on half of some aspects of them: https://docs.unity3d.com/Manual/Coroutines.html

    I had a quick dig through my Unity bug reports to see if I could find the most recent one about invocation delays, but it looks like it was after the move away from fogbugz, so getting at the bug report is now non-trivial (got to juggle different accounts, the reports are no longer public).
     
    Kurt-Dekker likes this.
  22. PraetorBlue

    PraetorBlue

    Joined:
    Dec 13, 2012
    Posts:
    7,909
    If you're talking about when a
    yield return null;
    for example will resume, absolutely yes it matters when in the frame you yielded, because it will resume the next time the "resume all yield return null coroutines" part of the frame comes along. but this stuff is documented in that diagram. In fact I'd argue that the question of when
    Start()
    runs on a MonoBehaviour that you Instantiated is more mysterious than when a coroutine will continue execution after a yield. At least the coroutine has well defined points in the diagram for resumption.

    StartCoroutine will still begin execution of the coroutine immediately, regardless of when you call it.
     
    Last edited: Jan 30, 2023
  23. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    38,735
    That would be interesting... obviously you can just do your own counter and delegate... or maybe your own object that is a "Joiner" that contains the MonoBehaviour instance that you're running from... something like:

    Code (csharp):
    1. Joiner j = new Joiner(this);  // give it the MonoBehaviour
    2.  
    3. j.onJoined += WeAreAllFinished;   // perhaps even give it to the ctor above?
    4.  
    5. j.StartCoroutine( Step1());
    6. j.StartCoroutine( Step2());
    7. j.StartCoroutine( Step3());
    Then inside it would observe each IEnumerator to see when all are exhausted??

    Or perhaps supply its own wrapper IEnumerator that runs the inner one and then notes the exit?

    I think it's all doable... it's just which one actually adds enough value to be worth the extra shaggy parts all over the place.
     
  24. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,999
    No, there are never implicitly any yield instruction inserted. This is just plain C# compiler magic and has nothing to do with Unity. Iterator blocks / generator methods are meant to "produce" a sequance of values. That's their main point. Unity uses this mechanic to implement coroutines. The "values" that are produced just indicate to the scheduler what to do next with the coroutine while all the "in between" code is your actual code you care about.

    I've written a coroutine crash course which explains most of the behind the scenes stuff. Though it doesn't really go into details how the scheduler works internally. Since UA has messed up its markdown rendering, I have a mirror on github.

    Yes, it's true that there are a few edge cases when coroutines get re-scheduled. Though most of these edge cases belong to the actual startup oddities. That means frame 0 usually does not count as a real "frame" as it acts purely as an initialization stage. In frame 0 only Awake and OnEnable are called on objects loaded in the start scene. No Start or Update call would happen in frame 0 and no coroutines would be resumed (there aren't any yet). In addition Frame 1 is the first "frame" but nothing is rendered during that frame. The first rendered frame is frame 2. This has some implications on how WaitForEndOfFrame works.

    Though in general the different yield values can be put in 3 categories:
    1. FixedUpdate
    2. Normal Update
    3. EndOfFrame
    The first FixedUpdate point is actually called exactly the way described in the execution diagram. So when you start a coroutine in OnEnable, Awake or Start and yield a WaitForFixedUpdate, it would be resumed at the next FixedUpdate call. This "can" happen in the same frame, but due to the nature of FixedUpdate, it may be scheduled multiple times in a frame or not at all. Well just usual FixedUpdate.

    The second category covers almost all possible yield values. That includes "null", WaitForSeconds, waiting for other coroutines, etc. This category is "always" continued at least the next frame. So those are never resumed in the same frame. So a coroutine started in Awake, OnEnable, Start, Update or LateUpdate will never be resumed in the same frame.

    The third category is the EndOfFrame resume point. This is directly tied to the rendering engine and is always resumed this frame when the rendering has finished.

    Those rules combined with the startup oddities I mentioned above can cause some confusion during the very first 3 frames. Since frame 0 and frame 1 do not render to the screen, coroutines started in frame 0, 1 or 2 would be all resumed at the end of frame 2, the first frame that actually renders something.

    Coroutines themselfs are just iterators and are simply resumed at the specified points. The only edge case is the starting of the coroutine as well as the startup specific anomalies as I just explained.

    If you read my coroutine crash course you should know that a coroutine is not a method, but actually a state machine object that implements an iterator. When you call StartCoroutine you just hand over that iterator to Unity. Unity will store that iterator internally and start "iterating" it. Based on the yielded value the scheduler decides what to do next with the coroutine.

    Feel free to add some Debug.Logs to your coroutine and include Time.frameCount to see in what frame the coroutine is actually resumed.
     
  25. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,999
    I like the idea, so I quickly created one :)

    Code (CSharp):
    1. public class Joiner : IEnumerator
    2. {
    3.     private class Item : IEnumerator
    4.     {
    5.         public IEnumerator enumerator;
    6.         public Coroutine coroutine;
    7.         public bool HasFinished { get; private set; }
    8.         public object Current => enumerator.Current;
    9.         public Item(IEnumerator aCoroutine)
    10.         {
    11.             enumerator = aCoroutine;
    12.         }
    13.         public bool MoveNext()
    14.         {
    15.             bool res = enumerator.MoveNext();
    16.             HasFinished = !res;
    17.             return res;
    18.         }
    19.         public void Reset() { }
    20.     }
    21.  
    22.     public object Current => null;
    23.     private List<Item> m_Coroutines = new List<Item>();
    24.     private MonoBehaviour m_Owner;
    25.     public bool AllFinished
    26.     {
    27.         get
    28.         {
    29.             foreach (var item in m_Coroutines)
    30.                 if (!item.HasFinished)
    31.                     return false;
    32.             return true;
    33.         }
    34.     }
    35.     public Joiner(MonoBehaviour aOwner)
    36.     {
    37.         m_Owner = aOwner;
    38.     }
    39.  
    40.     public Coroutine StartCoroutine(IEnumerator aCoroutine)
    41.     {
    42.         var item = new Item(aCoroutine);
    43.         m_Coroutines.Add(item);
    44.         return item.coroutine = m_Owner.StartCoroutine(item);
    45.     }
    46.     public void StopCoroutine(Coroutine aCoroutine)
    47.     {
    48.         foreach(var item in m_Coroutines)
    49.         {
    50.             if (item.coroutine == aCoroutine)
    51.             {
    52.                 m_Owner.StopCoroutine(aCoroutine);
    53.                 m_Coroutines.Remove(item);
    54.                 return;
    55.             }
    56.         }
    57.     }
    58.     public void StopCoroutine(IEnumerator aCoroutine)
    59.     {
    60.         foreach (var item in m_Coroutines)
    61.         {
    62.             if (item.enumerator == aCoroutine)
    63.             {
    64.                 m_Owner.StopCoroutine(item);
    65.                 m_Coroutines.Remove(item);
    66.                 return;
    67.             }
    68.         }
    69.     }
    70.  
    71.     public bool MoveNext()
    72.     {
    73.         return !AllFinished;
    74.     }
    75.  
    76.     public void Reset() { }
    77. }
    The class essentially just contains a wrapper class for the actual task coroutines. Those are stored in a List. The Joiner class itself is also an IEnumerator which finishes once all tasks are finished. The task coroutines themselfs are run as independent coroutines, but wrapped in an "Item" class. So they run independent from each other in parallel. The wrapper checks if it has finished and remembers this state in the HasFinished property. The Joiner simply checks those properties.

    If you want to stop a coroutine from the outside (which I would never recommend), The Joiner has the same two StopCoroutine methods like those Unity provides, but those will take care of removing the Item from the internal List so they actually count as "finished".

    If can be used like this:

    Code (CSharp):
    1. IEnumerator SomeCoroutine()
    2. {
    3.     Joiner J = new Joiner(this);
    4.     J.StartCoroutine(Task1());
    5.     J.StartCoroutine(Task2());
    6.     J.StartCoroutine(Task3());
    7.     yield return J;
    8.     Debug.Log("All Tasks finished");
    9. }
     
  26. Bunny83

    Bunny83

    Joined:
    Oct 18, 2010
    Posts:
    3,999
    Note: Since Unity introduced the CustomYieldInstruction (which is also just a custom IEnumerator object), they have added support for "sub" coroutines. It's different from the usual nested coroutines as you can now simply yield any IEnumerator inside a coroutine and Unity would simply iterate the sub coroutine in place of the outer coroutine. Once the sub coroutine has finished, it would simple resume the outer coroutine. So technically sub coroutines do not start new coroutines but run them in the same coroutine.