Before we get into this topic, we should understand the Single-Thread models of Client applications. Main Thread or UI Thread this is the thread where the UI controls are created. On the server side, our applications are multi-channel because User-A should not wait while User-B is working, but synchronization and concurrency are a headache in such applications.

Is Multi-Thread Needed?

In a multi-channel program, we should know that; As hardware, the processor can run a single Thread at the same time, but a certain time passes for the Threads. This work is so fast that we see it as a synchronicity. The need on the client side depends on the complexity of your application, as technology is no longer a bottleneck today:

  • If there is no time-consuming task such as network, IO, AI, try not to use multi-threading because cross-thread operations do not have permission to access the UI thread and data synchronization becomes an annoying problem. Also it is very easy to lock the UI with “lock” or “deadlock”.
  • Conversely, if the application is complex, it would be correct to reduce the pressure on the Main Thread with multi-channel.
    Also, it shouldn’t spam Thread, too many threads slow down CPU operation, in these cases it should use ThreadPool or Threads that GC returns.

Coroutine Internal Principles

If we go back to Unity3D; Unity applications are also single-thread but for asynchronous operations Coroutine is provided by Unity3D. Coroutine actually executes in the main thread. It consists of Coroutine Ienumarator interface and Yield iterator block in Unity. With the Enumarator interface, Ienumarator includes three methods:

  • Current: Returns the object in its current position in the Collection.
  • MoveNext: takes the iterator to the next position in the collection, checks whether the collection is exceeded with the bool it returns.
  • Reset: Reset the current state.
    Yield, this is a bit of a confusing technology because the compiler does a lot of work for us, we can’t see its internal implementation. It is confusing as to the meaning of the word. Therefore, it would be more correct to say branching, taking out. In C#, built-in Collections are enumarable, that is, they implement the Ienumarable interface, so we can iterate with a foreach even in an array.
static void Main(string[] args)
{
    string[] animals = {"dog", "cat", "pig"};
    //Get enumerator
    var ie = animals.GetEnumerator();
    //Move to the next item, the default index=-1
    while (ie.MoveNext())
    {
        //Get current item
        Console.WriteLine(ie.Current);
    }
    Console.ReadLine();
}

Let’s imagine that we will do the above logic ourselves; we need to provide a customized enumerator for this job, and for this we need to implement the Ienumerator interface.

class AnimalSet : IEnumerable
{
    private readonly string[] _animals = {"the dog", "the pig", "the cat"};
    public IEnumerator GetEnumerator()
    {
        return new AnimalEnumerator(_animals);
    }
}

Now let’s implement the AnimalEnumerator iterator.

class AnimalEnumerator : IEnumerator
{
    private string[] _animals;
    private int _index = -1;

    public AnimalEnumerator(string[] animals)
    {
        _animals=new string[animals.Length];

        for (var i = 0; i < animals.Length; i++)
        {
            _animals[i] = animals[i];
        }
    }

    public bool MoveNext()
    {
        _index++;
        return _index<_animals.Length;
    }

    public void Reset()
    {
        _index = -1;
    }

    public object Current
    {
        get { return _animals[_index]; }
    }
}

If you understand what has happened so far, you can easily understand what yield does in the rest. You will also understand why the Coroutine in Unity is a so-called multi-thread. Below is a simple piece of code:

void Start()
{
    StartCoroutine(MyEnumerator());
    Debug.Log("finish");
}

private IEnumerator MyEnumerator()
{
    Debug.Log("wait for 1s");
    yield return new WaitForSeconds(1);
    Debug.Log("wait for 2s");
    yield return new WaitForSeconds(2);
    Debug.Log("wait for 3s");
    yield return new WaitForSeconds(3);
}

Did you notice the return type of the method? You may have noticed that you did not define an enumerator and did not implement the required interface. The problem lies in the yield keyword. C# has been using this keyword since 2.0 to simplify these operations and create enumerator blocks for you. The compiler creates the necessary iterators for us. Now let’s decompile it:

public class Test : MonoBehaviour
{
    private IEnumerator MyEnumerator()
    {
        UnityEngine.Debug.Log("wait for 1s");
        yield return new WaitForSeconds(1f);
        UnityEngine.Debug.Log("wait for 2s");
        yield return new WaitForSeconds(2f);
        UnityEngine.Debug.Log("wait for 3s");
        yield return new WaitForSeconds(3f);
    }

    private void Start()
    {
        base.StartCoroutine(this.MyEnumerator());
        UnityEngine.Debug.Log("finish");
    }

    [CompilerGenerated]
    private sealed class <MyEnumerator>d__1 : IEnumerator<object>, IEnumerator, IDisposable
    {
        private int <>1__state;
        private object <>2__current;
        public Test <>4__this;

        [DebuggerHidden]
        public <MyEnumerator>d__1(int <>1__state)
        {
            this.<>1__state = <>1__state;
        }

        private bool MoveNext()
        {
            switch (this.<>1__state)
            {
                case 0:
                    this.<>1__state = -1;
                    UnityEngine.Debug.Log("wait for 1s");
                    this.<>2__current = new WaitForSeconds(1f);
                    this.<>1__state = 1;
                    return true;

                case 1:
                    this.<>1__state = -1;
                    UnityEngine.Debug.Log("wait for 2s");
                    this.<>2__current = new WaitForSeconds(2f);
                    this.<>1__state = 2;
                    return true;

                case 2:
                    this.<>1__state = -1;
                    UnityEngine.Debug.Log("wait for 3s");
                    this.<>2__current = new WaitForSeconds(3f);
                    this.<>1__state = 3;
                    return true;

                case 3:
                    this.<>1__state = -1;
                    return false;
            }
            return false;
        }

        object IEnumerator.Current
        {
            [DebuggerHidden]
            get
            {
                return this.<>2__current;
            }
        }

		//....
    }
}

Let’s sharpen our lessons:

  • The yield keyword is a syntactic, so the compiler cannot see exactly what you are doing.
  • Compiler internally creates enumeration class “d__1" When Yield return is used as an enumeration, Current gets the result via MoveNext.

You should have a clear understanding of a number of things about Coroutine in Unity. I mentioned branching/exporting for yield earlier. Let’s see what I mean by this:

  • Branching: As we understand from the code we decompile, the compiler is proceeding by branching between the switch and the states.
  • Exporting: It exports a data by writing the obtained object before each branch to Current.