Coroutine Library in C#: Principles and Implementation

Hello everyone! Today I want to share a particularly interesting topic – the implementation of coroutines in C#. As a developer who often deals with asynchronous programming, I understand the importance of coroutines in enhancing program performance and simplifying code structure. Let’s explore how to implement a simple yet powerful coroutine library in C#!

What Are Coroutines?

Coroutines can be understood as a type of “pausable function”. Imagine when you are reading a novel and need to answer a phone call, you place a bookmark on the current page and continue reading after the call – this is the basic principle of coroutines! In programming, coroutines allow us to save the current state during execution and continue from that saved point later.

public interface ICoroutine
{
    bool MoveNext();
    void Reset();
    object Current { get; }
}

Implementing Coroutine State Machine

In C#, we can implement coroutines using the state machine pattern. Each state represents a certain point of execution in the program.

public class SimpleCoroutine : ICoroutine
{
    private int _state = 0;
    private object _current;

    public object Current => _current;

    public bool MoveNext()
    {
        switch (_state)
        {
            case 0:
                // First execution point
                _current = "Step 1";
                _state = 1;
                return true;
            case 1:
                // Second execution point
                _current = "Step 2";
                _state = 2;
                return true;
            case 2:
                // Coroutine ends
                return false;
            default:
                return false;
        }
    }

    public void Reset()
    {
        _state = 0;
        _current = null;
    }
}

Using Yield for Elegant Coroutines

C# provides us with a more elegant way to implement coroutines using the <span>yield</span> keyword. It automatically generates state machine code for us.

public class YieldCoroutine
{
    public IEnumerator<string> RunTask()
    {
        Console.WriteLine("Starting task");
        yield return "Step 1 completed";

        // Simulate time-consuming operation
        Thread.Sleep(1000);
        yield return "Step 2 completed";

        Console.WriteLine("Task ended");
    }
}
</string>

Coroutine Manager

To better manage coroutines, we need a coroutine manager:

public class CoroutineManager
{
    private List<ienumerator> _coroutines = new List<ienumerator>();

    public void StartCoroutine(IEnumerator coroutine)
    {
        _coroutines.Add(coroutine);
    }

    public void UpdateCoroutines()
    {
        for (int i = _coroutines.Count - 1; i >= 0; i--)
        {
            if (!_coroutines[i].MoveNext())
            {
                _coroutines.RemoveAt(i);
            }
        }
    }
}
</ienumerator></ienumerator>

Practical Application Example

Let’s look at a practical application example in a game:

public class GameController
{
    private CoroutineManager _coroutineManager = new CoroutineManager();

    public IEnumerator PlayerAttackSequence()
    {
        Console.WriteLine("Player starts attacking");
        yield return new WaitForSeconds(1.0f); // Wait for 1 second

        Console.WriteLine("Playing attack animation");
        yield return new WaitForSeconds(0.5f);

        Console.WriteLine("Dealing damage");
        yield return new WaitForSeconds(0.3f);

        Console.WriteLine("Attack ended");
    }

    public void StartAttack()
    {
        _coroutineManager.StartCoroutine(PlayerAttackSequence());
    }
}

Tips:

  1. Coroutines are not threads! They execute on the same thread but can elegantly handle asynchronous logic.

  2. When using coroutines, pay attention to memory management to ensure there are no coroutine leaks.

  3. In Unity game development, coroutines are particularly useful for implementing animations, delayed execution, and more.

Notes:

  • Runtime environment: .NET 6.0 and above

  • Namespaces: You need to include <span>System.Collections</span> and <span>System.Collections.Generic</span>

  • Performance considerations: The state machine implementation of coroutines incurs some performance overhead, but it is much smaller than the overhead of thread switching.

Unit Test Example

[TestClass]
public class CoroutineTests
{
    [TestMethod]
    public void TestSimpleCoroutine()
    {
        var coroutine = new SimpleCoroutine();
        Assert.IsTrue(coroutine.MoveNext());
        Assert.AreEqual("Step 1", coroutine.Current);
        Assert.IsTrue(coroutine.MoveNext());
        Assert.AreEqual("Step 2", coroutine.Current);
        Assert.IsFalse(coroutine.MoveNext());
    }
}

Friends, that’s all for today’s C# learning journey! Remember to write code, and feel free to ask questions in the comments. I wish everyone a pleasant learning experience and may your C# development journey go further! Code changes the world, and see you next time!

Leave a Comment