Coroutines: How are they efficient?

Anand Mishra
MindOrks
Published in
6 min readJan 16, 2020
Photo by Les Triconautes on Unsplash

In software development, the difficult part of our life has almost always been about threads. Especially for Android Developers, Threads have been a go-to solution for everything harmful to the sacred main thread. While it has been a solution to most of our problems it also has a bit of overhead. I will explain the overhead part later. Hang on!

Before I move on to talk about my findings, I think giving you a basic knowledge of coroutines is important and for that, I am not going to copy-paste the original definition from the wiki. I am also not going to mention that it has been there since 1958 in a way expecting you to feel a lot better in terms of understanding as I don’t think it will be right. It wasn’t for me when others did. So, let’s say:

Coroutines are light-weight threads

They can suspend and continue themselves and don’t need anything else to communicate the same to them except themselves!

That’s it! It’s as if someone told you to get a glass of water for them. It doesn’t matter how you achieve it. You could go to a thread, the job of which is to dispatch a glass of water to everyone that comes to it and get the task done. You might also find someone else standing in a queue and you have to wait for them to be done with. This is where suspension and continuation come in. When you reach there, you have to suspend yourself and once it is the right time you have to continue the task you were given. Now in this whole process, it could be the case that you be a part of multiple threads or you break your whole task into multiple sub-tasks(depends on the developer and the implementation) and each task got completed on different threads or maybe the same. It depends on how you want to execute it.

Coming to the point of how it is different from a thread or what are the overheads that are there when a thread comes into the picture and don’t when coroutines are involved.

When you start a thread, it just doesn’t start then and there. It needs to go to the Scheduler to give directions to it. To start, pause or stop.

What are the things that come along with a Thread when they are brought into existence?

  • Stack, PC, Registers
  • OS, with the help of Scheduler, getting involved to do the preemptive multitasking which in turn needs memory
  • The number of threads you can create based on cores of your CPU.
  • And also, the more threads you create the more time it is going to take to create new ones(not large enough if the number is less).

When you talk about Threads and also multiple of them, Scheduler from OS is responsible to let these threads do their job and if need be can also pause them so that the same resources can be used by other threads. So Schedulers over here act as an authoritarian to decide which thread to start and which to stop based on priority and time. Which is why Threads support preemptive multitasking.

But coroutines support cooperative multitasking.

In preemptive multitasking, Scheduler is the boss. It can decide which thread to continue and which to stop and also do the switching itself. The switching itself is a costly operation. So threads depend on them to complete the task. They always have no idea when they will get the time to execute their task or when it will be stopped to give others priority but in cooperative multitasking, a program can take ownership of doing things independently without anyone from outside being involved. This means a program can have multiple coroutines running inside and internally they can communicate among themselves related to their suspension and continuation. This is where coroutines are smarter. By the power of communicating among themselves without a third party being involved and also being light-weight, they are much more efficient than a Thread.

Let’s go through a very simple example to get an idea of what it means in terms of numbers.

I have set up a very basic example of creating a million threads and coroutines separately. The idea of which came from the original docs. The only task I am asking each one of them to do is to add and increment an Atomic Long variable. In case of coroutines, I have also created a HashSet to keep adding the distinct name of the current Thread they are running on. Don’t worry about the GlobalScope.launch syntax. Let’s just say in a way it is a block where you can run coroutines. We will explore these details in the next part.

Also, measureTimeMillis is a helper block that takes a code-block as a parameter and returns you the total time it took to execute the same. So we don’t need to initialize start time with currentTimeInMillis and subtract it with end time to get the total time. measureTimeMillis will do it for us.

Happy?

There is also a startMultipleCodeBlocks method that accepts an identifier, coroutine or thread and creates multiple of them, millions in our case, to do a task. I’ll be passing both the identifiers separately and we’ll see how much time they take.

I’ll start with THREAD_IDENTIFIER

Can you guess the total amount of time it takes to complete?

Think about it….

Pretty Please…

With sugar on top…

Alright:

Starting thread
Elapsed Time: 70433
500000500000

Yes, 70 seconds. Sometimes, it took 54s also. I intentionally didn’t add the part where names of threads are printed in this case because, no prizes for guessing, there are lots of them. These many threads took this much time to get created and do the job. It also depends on your machine. My machine details:

Processor: Intel Core i7 Speed: 2.2 GHz No. of Processors: 1 Total Core: 4 Memory: 16 GB

Now, I will be passing the COROUTINES_IDENTIFIER. Can you guess the total time?

Wild guess?

3.6 seconds!

👏 👏 👏 to Jeremy Strong. Watch Succession like now!
Starting coroutine
Size of distinctThreads: 9
distinctThreads: [DefaultDispatcher-worker-1, DefaultDispatcher-worker-6, DefaultDispatcher-worker-2, DefaultDispatcher-worker-3, DefaultDispatcher-worker-7, DefaultDispatcher-worker-4, DefaultDispatcher-worker-8, DefaultDispatcher-worker-5]
Elapsed Time: 3658
500000500000

Sometimes, the same thing took 1.6s. After comparison, in case of coroutines, I added the below line to check the name of different threads they are running on and also printed the same.

distinctThreads.add(Thread.currentThread().name)

Do you know why it took significantly less time?

While we created millions of threads they didn’t optimize their way to do their job. They just did what they were supposed to do only when Scheduler told them to. It gave life to all these threads, stopped them, resumed them whenever needed and all of it took 70 seconds.

And with Coroutines, yes, we created millions of them but all of them only needed 9 Threads to be created to complete their task as they always work on a limited thread pool. In each one of them, multiple coroutines went, did their job and started another based on their communication mechanism and got terminated.

You can think of threads as individuals doing their separate tasks only when the higher individual who is expected to manage resources tells them when to but coroutines can be thought of as a team that communicates among themselves without a higher individual, which is Scheduler in this case, being involved. Also, the Scheduler doesn’t have any idea about the larger picture. It just runs its algorithm based on priority and time slicing and shares resources to threads which when combined with creating them along with thread switching is a costly operation. Coroutines bypass them and that is why it is more efficient.

Yes, in real-life scenarios we will not be creating millions of threads but the whole example gets the point across. And with this in mind, in future, if you have to do a background task, would you still go with creating a thread instead of coroutine?

Alright! It is your call. Take that informed decision if you have to. It is not as if we didn’t have any option when coroutines were not there.

The intention of this post was to give you a basic understanding of how and where coroutines are helpful. In the next part, as much as possible, I will try to cover the terminologies you need to know to use coroutines. There are many of them: Suspend, Scope, Blocks and last but not the least Structured Concurrency.

Feel free to hit me up on any of these in case you want to have a chat:

☺️ Twitter ☺️LinkedIn ☺️GitHub ☺️

Watch Succession!

--

--