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;
}

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?