- Home /
Inconsistent StopCoroutine behavior with CustomYieldInstruction
I noticed that I tried to use StopCoroutine on a coroutine that was waiting on a CustomYieldInstruction and that coroutine did not stop. The behavior I am seeing is inconsistent: most times the coroutine stops properly. However, if I am yielding on a series of nested IEnumerators, then the coroutine runs until the outer IEnumerator.
Does anyone have insights about why this is happening? It looks to me like a unity bug, but maybe I am misunderstanding something fundamental.
Here's a test monobehavior. It "should" print 3 lines "Starting Coroutine" "TestRoutine Started" "Stopping Coroutine". However, it prints 7 lines "Starting Coroutine" "TestRoutine Started" "Stopping Coroutine" "Post CustomYield" "PostCustomYield2" "PostCustomYield3" "PostCustomYield4"
using System;
using System.Collections;
using UnityEngine;
public class CustomYieldTest : MonoBehaviour
{
private bool _testStarted;
private Coroutine _coroutine;
public bool _stopMe;
public void Update()
{
if (!_testStarted)
{
_testStarted = true;
Debug.LogWarning($"Starting Coroutine {Time.frameCount}");
_coroutine = StartCoroutine(TestRoutine());
}
else if (_stopMe)
{
Debug.LogWarning($"Stopping Coroutine {Time.frameCount}");
StopCoroutine(_coroutine);
_stopMe = false;
}
}
private IEnumerator TestRoutine()
{
Debug.LogWarning($"TestRoutine Started {Time.frameCount}");
//Change this to use TestRoutineInternal2 and none of the "Post CustomYield" logs occur
yield return TestRoutineInternal(this);
Debug.LogWarning($"Post CustomYield5 {Time.frameCount}");
yield return null;
Debug.LogWarning($"Post CustomYield6 {Time.frameCount}");
}
private IEnumerator TestRoutineInternal(CustomYieldTest parent)
{
yield return TestRoutineInternal2(parent);
Debug.LogWarning($"Post CustomYield3 {Time.frameCount}");
yield return null;
Debug.LogWarning($"Post CustomYield4 {Time.frameCount}");
}
private IEnumerator TestRoutineInternal2(CustomYieldTest parent)
{
parent._stopMe = true;
//Change this to 'yield return null;' and none of the "Post CustomYield" logs occur
yield return new CustomYieldImplementation(1f);
Debug.LogWarning($"Post CustomYield {Time.frameCount}");
yield return null;
Debug.LogWarning($"Post CustomYield2 {Time.frameCount}");
}
}
public class CustomYieldImplementation : CustomYieldInstruction
{
private DateTime _timeToStop;
public CustomYieldImplementation(float seconds)
{
_timeToStop = DateTime.Now.AddSeconds(seconds);
}
public override bool keepWaiting => DateTime.Now < _timeToStop;
}
Answer by Bunny83 · Mar 04, 2020 at 02:04 AM
Ok I just have rewritten my entire answer since there were some mistakes in it ^^.I'm not sure if it always was like this since Unity supported yielding on IEnumerators. However currently it seems that yielding an IEnumerator will not start a new nested coroutine. Instead the coroutine scheduler seems to just chain the statemachines in the same coroutine. So you still have only one coroutine. When you yield on a nested IEnumerator the coroutine probably just stores the IEnumerator internally as the current active one (they might use a stack for that).
I just ran your test with both, your custom yield instruction and just yielding null. However it doesn't change the behaviour at all. I don't get any of your "Post CustomYield" logs since in the very first run you immediately set your "_stopMe" variable to true your coroutine will be stopped the next frame when Update runs. So your coroutine will never get past the first yield statement. Once the coroutine is stopped it will just vanish.
I can not reproduce your output given your code. Maybe you had your parent._stopMe = true;
line originally after your first yield statement? However in this case you could only see the first "Post CustomYield" inside your "TestRoutineInternal2". After that your coroutine would be stopped and nothing else could actually execute. Again I don't see any change when I replace your yield return new CustomYieldImplementation(1f);
with yield return null;
. If the _stopMe line is after that statement the only difference is that it takes 1 second before the coroutine is stopped when your custom yield instruction is used.
Note I carried out my tests in Unity 2019.0.3f6. Maybe you use a different Unity version?
Wow. I did not know that. I'm still a bit confused though.
Why if I change line "yield return new CustomYieldImplementation(1f);" to "yield return null;" does it not print any of the "Post CustomYield" lines? Shouldn't it still print the 4 "Post CustomYield" lines?
Note that I've run some tests with your code and I have re-written my answer since the behaviour might have changed in the past.
$$anonymous$$y original test was on 2018.4.3f1LTS. I confirmed once again using this code there prints 4 "Post CustomYield" statements.
The exact same code on 2019.3.3f1 prints 0 "Post CustomYield" statements.
It appears unity does not (generally) start separate coroutines for yielding on nested IEnumerators, even in 2018.4. I had to work hard to get an example that prints the "Post CustomYield" statements: if you remove one layer of nesting it does not work; if you yield return null, it does not work. To my eyes this looks like an inconsistency in 2018.4, I can not explain why it behaves like its starting separate coroutines only in this very specific instance.