avatarRoman Elizarov

Summary

The web content discusses the importance of explicit concurrency in modern applications, particularly in Kotlin, where concurrent execution is managed through coroutine builders and the CoroutineScope interface, emphasizing structured concurrency and the avoidance of GlobalScope.

Abstract

Concurrency is a critical aspect of modern software development, enabling applications to handle multiple tasks simultaneously, such as network requests and user interactions. The article highlights how Kotlin addresses concurrency through coroutines, which allow for sequential-looking code that executes concurrently without blocking threads. It emphasizes the use of CoroutineScope to initiate concurrent operations explicitly, ensuring that such operations are contained and managed according to the application's lifecycle. The article also warns against using GlobalScope for concurrent tasks, as it can lead to resource leaks and loss of track of concurrent activities. Instead, it advocates for structured concurrency, which helps maintain control over concurrent operations and aligns them with the application's components' lifecycles.

Opinions

  • The author believes that concurrency is essential for modern applications but is often a source of bugs due to its complexity.
  • Kotlin's approach to concurrency through coroutines is seen as beneficial because it allows for sequential coding patterns while enabling concurrent execution.
  • The article suggests that every function declared as an extension on CoroutineScope should return immediately but perform its actions concurrently, establishing a clear convention for developers.
  • There is a strong opinion against using GlobalScope for starting concurrent tasks, as it is considered risky for resource management and can lead to memory leaks.
  • The author advocates for the use of structured concurrency to contain and manage concurrent operations effectively, which is a principle deeply integrated into Kotlin's coroutine design.
  • The preference for suspending functions over GlobalScope is highlighted, as they provide a safer and more controlled way to handle concurrency.

Explicit concurrency

Concurrency is important in the modern world. A mobile or web application needs to be able to perform a network request and still concurrently update its UI, display animations, and react to user input. Server-side application needs to be able to handle many concurrent requests. What about parallelism? It is useful too, but not that much often. Many successful server-side applications run totally in a single thread without any parallelism, yet scale quite well (think about the whole node.js platform).

Even without parallelism, concurrency is hard and can be a source of subtle timing-dependent bugs. Developers are used to sequential programs that do things one after another, and it does not matter if they code in functional or imperative style. The whole concept of a function call is sequential — you call a function and, after a while, it returns a result that you can use. Look at the following code:

foo(bar())

It is just an expression and it looks very functional, yet it is a good example of a sequential code. Its sequential nature becomes obvious if we rewrite it in a more imperative style, like this:

val x = bar()  // first call bar
foo(x)         // then call foo

In “Blocking threads, suspending coroutines” story I’ve shown a difference between regular functions and suspending functions in Kotlin. The latter don’t block a thread, but they still execute sequentially. For example, if bar is suspending, then the above examples are still sequential and foo executes only after bar successfully returns.

Concurrency, on the other hand, is very explicit in Kotlin. You explicitly use a coroutine builder like launch { ... } to initiate concurrent execution of some piece of code with the respect to the rest of the program:

launch {
    foo(bar()) 
}

However, every concurrent coroutine builder in kotlinx.coroutines library is declared as an extension on CoroutineScope interface and launch is no exception. So, if you extract the code that initiates concurrent execution of foo(bar()) into a separate top-level function, then you get the function with the following signature:

fun CoroutineScope.launchFooBar() = launch { 
    foo(bar())
}

This function returns immediately and executes foo(bar()) concurrently with the rest of the program that calls launchFooBar.

It leads to the following useful convention: every function that is declared as extension on CoroutineScope returns immediately, but performs its actions concurrently with the rest of the program.

It also explains one of the reasons why runBlocking is NOT an extension function on CoroutineScope.

Following this convention in your own code is very helpful, too. It is easy to show with a counter-example. Consider the function with the following signature that you could stumble upon somewhere in the code you work with:

suspend fun CoroutineScope.obfuscate(data: Data)

What could this function do? It is a suspending function, so we know, by definition, that it could suspend execution of a coroutine. But it is also defined as an extension on CoroutineScope, so it could launch a new coroutine to do something that would work concurrently with the rest of the program. Why would it suspend execution then? What parts of its operation are performed concurrently? We cannot tell without reading its documentation and/or source code. On the other hand, if we choose to declare it as a suspending function:

suspend fun obfuscate(data: Data)

Then, by convention, we know that it does its obfuscate thing without blocking the caller and returns to the caller when it’s done.

Alternatively, if we choose to declare it as a CoroutineScope extension:

fun CoroutineScope.obfuscate(data: Data)

Then, by convention, we know that it returns immediately without blocking the caller and starts doing its obfuscate thing concurrently with the rest of the program.

As a rule of thumb, you should prefer suspending functions, since concurrency is not a safe default to have. Anyone with a suspending function at hand can explicitly use launch { ... } to initiate its concurrent execution.

Global scope

It is tempting to write GlobalScope.launch { ... } when you need to start doing something concurrently but your code is not inside any CoroutineScope. It is as easy as submitting a new task to the global background thread pool. Resist this temptation. It is not hard to lose track of concurrent activities you have, run out of resources and/or introduce memory leaks this way (see more in “The reason to avoid GlobalScope”).

Explicit use of CoroutineScope lets you contain, delimit and keep track of all the concurrent operations in your application and, importantly, tie them to the lifecycle of your application entities. But that is another story (see “Coroutine Context and Scope”).

Further reading

The reasoning that led to introduction of the CoroutineScope and structured concurrency in Kotlin coroutines was explained in “Structured concurrency” story. It also shows how suspending functions can perform multiple concurrent operations while still maintaining invariant that they return to the caller only when they’ve done doing their thing.

If you are interested in a formal definition of concurrent execution and how it is different from sequential, then read my totally approachable introduction to this hard theoretical problem in this story on concurrency I wrote a while ago.

More details on GlobalScope can be found in the reason to avoid GlobalScope story.

Programming
Kotlin
Coroutine
Concurrency
Recommended from ReadMedium