Whats the difference between a coroutine and a thread? Are there any advantages of using one over the other?
While Coroutines seem to work like threads at first glance, they actually aren't using any multithreading. They are executed sequentially until they
yield. The engine will check all yielded coroutines as part of its own main loop (at what point exactly depends on the type of
yield, check this diagram for more information), continue them one after another until their next
yield, and then proceed with the main loop.
This technique has the advantage that you can use coroutines without the headaches caused by problems with real multithreading. You won't get any deadlocks, race conditions or performance problems caused by context switches, you will be able to debug properly and you don't need to use thread-safe data containers. This is because when a coroutine is being executed, the Unity engine is in a controlled state. It is safe to use most Unity functionality.
With threads, on the other hand, you have absolutely no knowledge about what state the Unity main loop is in at the moment (it might in fact no longer be running at all). So your thread might cause quite a lot of havoc by doing something at a time it isn't supposed to do that thing. Do not touch any native Unity functionality from a sub-thread. If you need to communicate between a sub-thread and your main thread, have the thread write to some thread-safe(!) container-object and have a MonoBehaviour read that information during the usual Unity event functions.
The disadvantage of not doing "real" multithreading is that you can not use coroutines to parallelize CPU-intense calculations over multiple CPU cores. You can use them, though, to split a calculation over multiple updates. So instead of freezing your game for one second, you just get a lower average framerate over multiple seconds. But in that case you are responsible to
yield your coroutine whenever you want to allow Unity to run an update.
- If you want to use asynchronous execution to express game logic, use coroutines.
- If you want to use asynchronous execution to utilize multiple CPU cores, use threads.
Coroutines are what we in Computer Science call "cooperative multitasking." They are a way for multiple different streams of execution to interleave with eachother cooperatively. In cooperative multitasking one stream of execution has sole undisputed ownership of the CPU until it reaches a
yield. At that point, Unity (or whatever framework you are using), has the option to switch to a different stream of execution. It then also gets sole undisputed ownership of the CPU until it
Threads are what we call "preemptive multitasking." When you are using threads, the framework reserves the right at any time to stop your thread mid-thought and switch over to another thread. It doesn't matter where you are. You can even be stopped part way through writing a variable to memory in some cases!
There are pros and cons for each. The cons of coroutines are probably the easiest to understand. First off, coroutines all execute on a single core. If you have a quad core CPU, coroutines will only use one of the four cores. This simplifies things, but can be a performance issue in some cases. The second con is that you must be aware that any coroutine can stop your entire program simply by refusing to
yield. This was an issue on Mac OS9, many years ago. OS9 only supported cooperative multitasking, across the entire computer. If one of your program hung, it could halt the computer so fiercely that the OS couldn't even render the text of the error message to let you know what happened!
The pros of coroutines is that they're relatively easy to understand. The errors you have are far more predictable. They also typically call for fewer resources, which can be helpful as you climb up into the 10's of thousands of coroutines or threads. Candid Moon mentioned in the comments that, if you haven't studied threads properly, just stick to coroutines, and they're right. Coroutines are much simpler to work with.
Threads are a different beast entirely. You always have to be on guard against the possibility that another thread may interrupt you at any time and mess with your data. Threading libraries provide entire suites of powerful tools to assist you with this, such as mutexes and condition variables which help you tell the OS when it's safe for it to run one of your other threads and when it is unsafe. There's entire courses dedicated to how to use these tools well. One of the famous issues that arises is a "deadlock," which is when two threads both get "stuck" waiting for the other to free some resources. Another issue, which is very important for Unity, is that many libraries (like Unity) are not designed to support calls from multiple threads. You can very easily break your framework if you don't pay attention to which calls are permitted and which ones are forbidden.
The reason for this extra complexity is actually very simple. The preemptive multitasking model is really similar to the multithreading model which permits you to not only interrupt other threads, but to actually run threads side-by-side on different cores. This is incredibly powerful, being the only way to really leverage these new quad core and hex code CPUs that are coming out, but opens up pandoras box. The synchronization rules for how to manage this data in a multithreading world are positively brutal. In the C++ world, there's entire articles dedicated to
MEMORY_ORDER_CONSUME which is one itty-bitty-teeny-weenie corner of multithreading synchronization.
So the cons of threading? Simple: they're hard. You can come across entire classes of bugs you've never seen before. Many are so-called "heisenbugs" that sometimes appear, and then dissapear when you debug them. The tools you are given to deal with these are very powerful, but they're also very low level. They're designed to be efficient on the architectures of modern chips rather than being designed to be easy to use.
However, if you want to use all your CPU power, they're the tool you need. Also, there are actually algorithms that are easier to understand in multithreading than they are with coroutines simply because you let the OS handle all the questions of where interruptions can take place.
Candid Moon's comment to stick to coroutines is my recommendation as well. If you do want the power of threads, then commit to it. Go out and really learn threads, formally. We've had several decades to figure out how to organize the best way to think about threads so that you get safe reliable repeatable results early, and add performance as you go. For example, all sane courses will teach mutexes before teaching condition variables. All
sane courses that cover atomics will fully teach mutexes and condition variables before even mentioning that atomics exist. (Note: there is no such thing as a sane tutorial on atomics.) Try to learn threading piecemeal, and you're begging for a migraine.
In the simplest terms possible...
A thread does not decide when it yields, the operating system (the 'OS', e.g. Windows) decides when a thread is yielded. The operating system is almost entirely responsible for scheduling threads, it decides what threads to run, when to run them and for how long.
Additionally a thread might be run synchronously (one thread after another) or asynchronously (different threads running on different CPU cores). The ability to run asynchronously means threads can get more work done in the same amount of time (because the threads literally are doing two things at the same time). Even synchronous threads get lots of work done if the OS is good at scheduling them.
However, this extra processing power comes with side effects. For example, if two threads are trying to access the same resource (e.g. a list) and each thread can be randomly stopped at any point in the code, the second thread's modifications could interfere with the modifications made by the first thread. (See also: Race Conditions and Deadlock.)
Threads are also considered to be 'heavy' because they have a lot of overhead which means there's a considerable time penalty incurred when switching threads.
Unlike threads, coroutines are completely synchronous, only one coroutine can be running at any point in time. Additionally coroutines get to choose when to yield, and thus can choose to yield at a point in the code that is convinient (e.g. at the end of a loop cycle). This has the advantage of issues like race conditions and deadlocks much easier to avoid, as well as making it easier for coroutines to cooperate with each other.
However this is a major responsibility too, if a coroutine doesn't yield properly it could end up consuming a lot of processor time and it can still cause bugs if it modifies shared resources incorrectly.
Coroutines generally require no context switching and are thus quick to switch in and out of and are quite lightweight.
- Synchronous or Asynchronous
- Yielded by OS
- Yielded randomly
- Yields self
- Yields by choice
The roles of threads and coroutines are very similar, but they differ in how they get the job done which means that each is better suited to different tasks. Threads are best for tasks where they can focus on doing something on their own without being interrupted, then signalling back when they're done. Coroutines are best for tasks that can be done in lots of small steps and tasks that require cooperatively processing data.