Amit Nadiger
11 May 2022
•
6 min read
Have you heard of Routine or Subroutine in programming languages? It simply means functions or methods . Co-Routines means cooperative routines . i.e routines which cooperate with each other . It means when one routine is executing the other routine will not interfere with 1st coroutine w.r.t to memory , cpu ,any resource by suspending itself i.e without blocking. I will talk about suspend and blocking a little later , please hold on ! Coroutines are supported by many languages such as Kotlin ,C++(20 onwards), java scripts ,etc . The concept of coroutine is almost the same across all languages , but there will be some differences. Here I will talk more about the Kotlin coroutine but the concepts remain the same across all languages .
Why are Coroutines required , is thread or process not enough ?
Coroutines are lightweight compared to Process and Threads . Wait a minute what you mean by light or heavy weight ?
Note : Below are w.r.t unix/Linux ( The definition of process , task , thread may differ based on the OS or platform )# Process creation : As you might know the process creation is a very heavy process (means it takes more cpu time)which will involve copying the parent’s variables, memory and CPU registers into the child process based on COW(Copy on write ). The new process will have a separate address space i.e stack , heap ,pc, and data segment, file descriptors, signal actions than the child process . The 1st process(namely init) is created by hand at the start up time and then onwards any subsequent process is created using the fork() call in *inux system which involves copying the parent process. # IPC: Inter-process communication is also very heavy since each process has a separate heap and data segment , so IPC such as shared memory , MQ ,etc , need to be used.# Context switching: Context switching between processes is heavy since entire copies of stack , cpu registers heap ,stack ,data segment of the current process need to be saved on secondary memory or virtual memory and all the memory of new or schedules process need to be copied or loaded into main memory before execution . This is the post process of scheduling. Processor’s cache and TLB gets flushed when virtual memory spaces change in process , making memory accesses much more expensive.
Thread creation is the same as a process at the kernel level since both are represented as tasks(task_struct ) at the kernel level and creation is done using clone(). Linux implements all threads as standard processes. Thread is merely a process that shares certain resources with other processes (which just happens to share resources, such as an address space(DS,heap , signal action , FD,etc ), with other processes). Threads are created at the user space by pthread_create() (Posix call) Ex: pthread_create(pthread_t thread_id,pthread_attr_t attributes, void (ThreadFunction)(void), void* ThreadParamToBePassedToThreadFunction) One process contains at least one thread and can contain more than one thread. Please note that threads within the same process have separate stack , cpu registers ,program counter etc .Conclusion : Thread creation is much cheaper compared to process.# IPC: Since threads operate within the same address space i,.e they share the DS,heap , signal action , FD,etc within the same process . One simple use case is : Threads can share the global variables , One thread can write and another thread can read the same global variables , thus can communicate with each other . Of -Course threads within the same process should be synchronized and also protect the common resource to avoid the read and write at the same time i.e we call it as corruption. Synchronization can be achieved using resources such as semaphore , message queues, combined usage of Mutex and conditional variables , and synchronized keywords in java . For achieving synchronization signaling is must i.e one thread must signal other thread once they are done with the resource , so that other thread can unblock and start working. Resource protection can be using mutex, semaphore ,spin locks ,critical section etc . Critical section means section of the code segment /function which modifies or access the critical resource i.e shared resource(Ex; global or heap variable) .Such section of must be guarded by resource protection such as mutex or locks. , so that only one process/thread can execute in its critical section at a time.## Conclusion : IPC in threads is much cheaper than process Context switching : Since address space(Heap,DS,FD,) or virtual memory space remains the same between threads in a same process takes less time compared to process switching . Processor’s cache and TLB(Translational Lookaside Buffer) don't change their state , hence it is fast and efficient and effectively less costly . I am planning to write a separate article on Process , Thread , IPC, scheduling in future .
In android, if the main or UI thread cannot finish executing blocks of work within 16ms, the user may observe hitching, lagging, or a lack of UI responsiveness to input. If the main thread blocks for approximately five seconds, the system displays the Application Not Responding (ANR) dialog, allowing the user to close the app directly. So the solution for the above problem is async programming. i.e don't block the main thread . Delegate the heavy work out of the main thread using any of the below: Threads Futures and Promises Async task (deprecated from Android 11) Reactive Extensions (Rx) Coroutines <-- Lets talk about this !
Coroutines are lighter than threads . Since they stack less . I.e coroutines don't have a dedicated stack . It means coroutine suspend execution by returning to the caller and the data that is required to resume execution is stored separately from the stack. This allows for sequential code that executes asynchronously (e.g. to handle non-blocking I/O without explicit callbacks). In an android environment specially in phones where there is constraint for memory , cpu , battery and app response to user action is very critical. Coroutines are light-weight threads & Using coroutines can remove callbacks. In Kotlin coroutines can be launched with launch,async ,runBlocking, withcontext coroutine builder in a context of some CoroutineScope. . By default, coroutines are run on a shared pool of threads. Please note that Threads still exist in a program based on coroutines. One thread can run many coroutines, so there's no need for too many threads. One Coroutine may run many threads i.e. shared pool of threads .and also many coRoutines can run one threads. Coroutine doesn't have a dedicated stack, they share the stack due to support for suspension,. Coroutine offers fewer memory leaks due to use of structured concurrency i.e It is a way to structure async/concurrent tasks so that child operations are guaranteed to complete before their parents, i.e. no child operation is executed beyond the scope of a parent scope. That means coroutines can be only launched in a specific CoroutineScope. And also cancellation is propagated automatically through the running coroutine hierarchy i.e from parent to all the children. Coroutines solve problems of callback such as complexity in readability of code due to many callbacks & also some language dony support the exception handling in callbacks. Under the hood of coroutine -> How are Coroutines built ? Coroutines build upon regular functions by adding two new operations in addition to invoke (or call) and return, coroutines as below :
First of all let's understand the meaning of blocked and suspend w.r.t process or thread .
A process being suspended means that the OS has stopped executing it, but that could just be for time-slicing (multitasking). Suspending functions can only be called from another suspending function or from a coroutine. In android UI applications there is usually the single main thread that handles all the UI interactions and events. Blocking this thread makes the whole application unresponsive . Hence suspension comes to rescue. Function that can be paused and resumed at a later time , hence suspended function can execute a long running operation and wait for it to complete without blocking .The suspending feature of these functions helps us to write asynchronous code in a synchronous manner.
Coroutines can be used for non-preemptive multitasking, by allowing execution to be suspended and resumed. Coroutines enable you to write clean, simplified asynchronous code that keeps your app responsive while managing long-running tasks. Coroutines are well-suited for implementing familiar program components such as cooperative tasks, exceptions, event loops, iterators, infinite lists and pipes.
Please see my next article about details of Kotlin Coroutine creation , usage and various concepts. Please comment if you have anything to add or modify . Thank you for reading !
Amit Nadiger
See other articles by Amit
Ground Floor, Verse Building, 18 Brunswick Place, London, N1 6DZ
108 E 16th Street, New York, NY 10003
Join over 111,000 others and get access to exclusive content, job opportunities and more!