Tutorials

Intro to Coroutines in Unity (C#)


 

In this demonstration, we'll be going over how Coroutines operate in Unity and how we can effectively use them in an application.

 

Concurrency

To understand how coroutines operate in Unity, it is essential to understand the concept of concurrency.

In the simplest form, concurrency is when two parts of a program operate at the same interval, but not at the same time. As an example, Unity runs Update() and LateUpdate() every frame draw. Although they both run at the same interval (every frame), LateUpdate always runs after Update during every frame.


Example Application

The following example showcases a component that reads user input without using Unity's Update method.

Additional information on how coroutines work with Unity can be found on Unity's official page for coroutines:

https://docs.unity3d.com/Manual/Coroutines.html

Intro to Multi-threading in Unity (C#)


 

In this demonstration, we'll be visualizing some of the basic concepts of multithreading and applying those concepts to make a simple comparison sequence.

 

Codeless Concepting

Although Unity technically uses multiple threads by itself, user-written code primarily runs on the same thread and for this exercise, that's what we're interested in. Let's call that thread the "Main" thread.

As shown at the top of Diagram 1, we'll be using up to 4 different threads during this demonstration, including the Main thread.

To measure how long any thread takes to complete an action, we will use arbitrary units of time ("time units"). Although how long an action takes is important when applying our concepts, using this abstract version of time is much easier to analyze and calculate with.

Some actions will have dependencies, these dependencies can be defined as actions that must complete before the current action can start.

Diagram 1: Empty Chart

Single-threaded Sequence

Before understanding when to efficiently apply multi-threaded systems, it's good to understand when not to use them. To do this, let's first define a sequence of actions (the "Action Sequence").

  • Action 1 ("A1")

    • Time: 2 units
  • Action 2 ("A2")
    • Dependencies: A1
    • Time: 2 units
  • Action 3 ("A3")
    • Dependencies: A1, A2
    • Time: 1 unit

Diagram 2: Single-threading

As the Action Sequence suggests, A1 occupies the Main thread for 2 units of time. After A1 completes, Main then executes A2 for another 2 units of time. After both A1 and A2 have been completed, Main executes A3 for 1 unit of time. In situations like this - where each task is dependent on all tasks before - the sequence can only be performed as a single-threaded sequence. This is because the actions only have one possible order of sequencing, meaning that although you could perform them on multiple threads, efficiency would not increase.

Multi-threaded Sequence

Now let's see how we can do more work in the same amount of time.

  • Action 1 ("A1")
    • Time: 3 units
  • Action 2 ("A2")
    • Time: 2 units
  • Action 3 ("A3")
    • Dependencies: A1, A2
    • Time: 2 units

Diagram 3: Basic Multi-threading

Although the total time of the sequence is 7 units, it only takes 5 units to complete. This is because A1 and A2 are able to run in parallel (at the same time) due to neither of them being dependent on each other. 


Example Application

Now let's apply those concepts of basic multi-threading into a simple example using Unity syntax.

  • Action 1 ("A1")

    • Goal: Add 100 to the first counter
  • Action 2 ("A2")
    • Goal: Add 100 to the second counter
  • Action 3 ("A3")
    • Dependencies: A1, A2
    • Goal: Check if counter one and counter two are equal

For simplicity's sake, we'll be using an Action Sequence similar to the "Basic Multi-threading" example shown above.

Since we're using Unity, we'll be starting at the beginning of the Start() function (Line 11). First things first, we're going to create the second thread ("T2") and tell it what action it'll be performing when it starts (in this case, A2). We then tell T2 to start working. 

It may seem a bit odd to start A2 before A1, but there's a good reason behind this. When you create additional threads (T2 in this case) you should make sure the other threads are able to start working as soon as possible; It's like telling everyone else to start on their work so you can get your own work done too.

Now that T2 has gotten to work, let's get to work on the Main thread. We tell the Main thread - which can also be thought of as the default thread - to perform A1 while Thread #2 is performing A2. 

What's just been done demonstrates the core idea of parallelism we've been touching on throughout the exercise: We've got two threads, running in parallel, performing two different tasks at the same time.

Because A3 is reliant on both A1 and A2 to have been completed we need a way of making sure both of those are done. Once Main has finished A1, we tell it to Join() (i.e., "wait for") our second thread.

If you call Join() on a thread that is already done with its work, Join() may seemingly appear to do nothing. On the contrary, it has already finished with its waiting as there is simply nothing to wait on.

A3 now executes on the Main thread and the rest is rather straightforward from there; The action sequence is now finished and has successfully ran using multi-threading.