12
\$\begingroup\$

Whats the difference between a coroutine and a thread? Are there any advantages of using one over the other?

\$\endgroup\$
21
\$\begingroup\$

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.

Conclusion:

  • 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.
\$\endgroup\$
  • \$\begingroup\$ Comments are not for extended discussion; this conversation has been moved to chat. \$\endgroup\$ – MichaelHouse Jul 7 '17 at 21:12
9
\$\begingroup\$

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 yields.

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.

\$\endgroup\$
  • \$\begingroup\$ Threads add complexity mainly in cases where operations and dependencies are interleaved. If the main game loop contains three functions, x(), y(), and z(), and none of them affects anything which is needed by another, starting all three of them simultaneously but then waiting for all to complete before proceeding may allow one to receive some benefit from a multi-core machine without having to add too much complexity. While condition variables are generally used in conjunction with mutexes, the independent-parallel-steps paradigm doesn't really need mutexes as such... \$\endgroup\$ – supercat Jul 6 '17 at 22:34
  • \$\begingroup\$ ...but simply a way for the threads that handle y() and z() to wait until they're triggered, and then a way for the main thread to wait, after running x(), until y() and z() have completed. \$\endgroup\$ – supercat Jul 6 '17 at 22:35
  • \$\begingroup\$ @supercat You always have to have some synchronization in order to transition between independent parallel phases and sequential phases. Sometimes that's nothing more than a join(), but you do need something. If you don't have an architect that designed a system to do the synchronization for you, you have to write that yourself. As someone who does multithreaded stuff, I find that people's mental models of how computers work need adjusting before they do synchronization safely (which is what good courses will teach) \$\endgroup\$ – Cort Ammon Jul 6 '17 at 23:56
  • \$\begingroup\$ Of course there needs to be some synchronization, and achieving top efficiency will generally require something more than join. My point was that pursuing a moderate fraction of the possible performance benefits of threading, easily, may sometimes be better than using a more complicated threading approach to reap a bigger fraction. \$\endgroup\$ – supercat Jul 7 '17 at 5:59
  • \$\begingroup\$ About new types of bugs: note also if you cannot logically prove code is safe, it is impossible to know how buggy it will be out the door. Although execution order can be undefined, it often takes similar paths every time you run it on 1 machine. You often will find that it fails hard and often on a fraction of machines, and you cannot possibly test on all possible machines. At work we had a bug that only manifested on 1 of our machines, only if logging was off; took multiple people a long time to track it down. Don't want that in the wild. Don't just test synchronization, logically prove it. \$\endgroup\$ – Aaron Jul 7 '17 at 16:19
2
\$\begingroup\$

In the simplest terms possible...


Threads

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.


Coroutines

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.


In Summary:

Thread:

  • Synchronous or Asynchronous
  • Yielded by OS
  • Yielded randomly
  • Heavy

Coroutine:

  • Synchronous
  • Yields self
  • Yields by choice
  • Lightweight

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.