Launching coroutines vs suspend functions
Let's consider a use case where you need to perform a number of concurrent actions. There are two kinds of functions that can be used for this:
- a regular function operating on a coroutine scope object,
- a suspending function.
These two options might look similar in some ways, but they represent essentially different use cases. To see some essential differences, you don’t even need to read their bodies.
When we know that a regular function starts coroutines, we know it needs to use a scope object. Such functions typically only start coroutines and don’t await their completion, so
sendNotifications will most likely take milliseconds to execute. Starting coroutines on an external scope also means that exceptions in these coroutines will be handled by this scope (most often printed and ignored). These coroutines will inherit context from the scope object, so we need to cancel the scope to cancel them.
When we know that a suspending function starts coroutines, we can assume that this function will not finish until all the coroutines are finished. Such
sendNotifications will suspend until the last notification is fully handled. This is an important synchronization mechanism. We also know that suspending functions don’t cancel their parent. Instead, they might throw or silence exceptions, just like regular functions can throw or silence exceptions. The coroutines started by such a function should inherit context from and build relationships with the function that called them.
Both these use cases are important in our applications. When we have a choice, we should prefer suspending functions as they are easier to control and synchronize. But coroutines need to start somewhere, and for that we use regular functions with a scope object.
In some applications, we might have a situation where we need to mix these two kinds of functions when we need suspending functions that also start external processes on an outer scope.
Consider a function that needs to execute some operations that are an essential part of its execution, but it also needs to start an independent process. In the example below, the process of sending an event is considered an external process, so it is started on an external scope. Thanks to this:
updateUserwill not wait until the
sendEventexecution is completed.
sendEventprocess will not be cancelled when the coroutine that started
updateUseris cancelled. (This makes sense because user synchronization has completed anyway.)
eventsScopedecides what the context should be for sending events, and whether or not this event should happen. (If
eventsScopeis not active, no event will be sent.)
In some situations, this hybrid behavior is what we want in our applications, but it should be used wisely.