Is coroutine a new thread in Unity3D?

Zening Qu picture Zening Qu · Jun 6, 2013 · Viewed 15.8k times · Source

I am confused and curious about how coroutines (in Unity3D and perhaps other places) work. Is coroutine a new thread? Unity's documentation they said:

A coroutine is a function that can suspend its execution (yield) until the given YieldInstruction finishes.

And they have C# examples here:

using UnityEngine;
using System.Collections;

public class example : MonoBehaviour {
    void Start() {
        print("Starting " + Time.time);
        StartCoroutine(WaitAndPrint(2.0F));
        print("Before WaitAndPrint Finishes " + Time.time);
    }
    IEnumerator WaitAndPrint(float waitTime) {
        yield return new WaitForSeconds(waitTime);
        print("WaitAndPrint " + Time.time);
    }
}

I have many questions about this example:

  1. In the example above, which line is the coroutine? Is WaitAndPrint() a coroutine? Is WaitForSeconds() a coroutine?

  2. In this line: yield return new WaitForSeconds(waitTime);, why both yield and return are present? I read in Unity documentation that "The yield statement is a special kind of return, that ensures that the function will continue from the line after the yield statement next time it is called." If yield is a special return, what is return doing here?

  3. Why do we have to return an IEnumerator?

  4. Does StartCoroutine start a new thread?

  5. How many times has WaitAndPrint() been called in the above example? Did yield return new WaitForSeconds(waitTime); really returned? If yes then I guess WaitAndPrint() was called twice in the above code. And I guess StartCoroutine() was calling WaitAndPrint() multiple times. However, I saw another Unity documentation that says: "The execution of a coroutine can be paused at any point using the yield statement. The yield return value specifies when the coroutine is resumed." These words make me feel that WaitAndPrint() actually has not returned; it was merely paused; it was waiting for WaitForSeconds() to return. If this is the case, then in the above code WaitAndPrint() was called only once, and StartCoroutine was just responsible for starting the function, not calling it multiple times.

Answer

spender picture spender · Jun 6, 2013

Coroutines are an extremely powerful technique used to emulate the kinds of features supported by the async/await in .net4.5, but in earlier versions ( c# >= v2.0 ) .

Microsoft CCR (take a read) also employs (employed?) this approach.

Let's get one thing out of the way. yield alone is not valid and is always followed by either return or break.

Think about a standard IEnumerator (that doesn't yield flow control messages).

IEnumerator YieldMeSomeStuff()
{
    yield "hello";
    Console.WriteLine("foo!");
    yield "world";
}

Now:

IEnumerator e = YieldMeSomeStuff();
while(e.MoveNext())
{
    Console.WriteLine(e.Current);
}

What's the output?

hello
foo!
world

Notice how, the second time we called MoveNext, before the Enumerator yielded "world" some code ran within the Enumerator. What this means is that in the Enumerator, we can write code that executes until it hits a yield return statement, then simply pauses until someone calls MoveNext (handily with all state/variables neatly captured, so we can pick up where we left off). After a MoveNext call, the next bit of code after the yield return statement can run until another yield return is reached. So we can now control the execution of the code between the yield return statements with the MoveNext call to the Enumerator.

Now, say instead of yielding strings, our Enumerator were to yield a message that says to the caller of MoveNext, "please hang around for x (waitTime) seconds before you call MoveNext again". The caller is written to "understand" a variety of messages. These messages will always be along the lines of "please wait for such and such to happen before calling MoveNext again".

Now we have a powerful means of pausing and restarting code that requires other conditions to be met before it can proceed, without having to write that functionality into another method, like doing async stuff without coroutines. Without coroutines, you're forces to pass around a horrible async state object that you would need to manually assemble to capture state between the end of one method and the starting of another after some async stuff). Coroutines eliminate this because scope is preserved (by compiler magic), so your local variables persist over long lived async stuff.

StartCoroutine simply starts the whole process. It calls MoveNext on the Enumerator... some code runs in the Enumerator... The enumerator yields a control message, which informs the code in StartCoroutine when to call MoveNext again. This need not happen in a new Thread, but can be handy in multithreaded scenarios because we can call MoveNext from different threads and control where the work is done.