Synchronous and Asynchronous runs: run, runCatching, runBlocking and runInterruptible in Kotlin | by First I have fika, then I write apps. | Dec, 2023
Recently, I watched a video about runBlocking. It’s a good explanation of runBlocking behavior.
In summary, runBlocking
documentation highlights several key limitations and recommendations:
This video effectively covered these points. In this post, however, I aim to dive deeper. In this post, I will try to understand what happens in more complex scenarios.
First of all, run
and runCatching
are synchronous, and runBlocking
and runInterruptible
as asynchronous. run
and runCatching
are part of the standard Kotlin library, available across all supported platforms. runBlocking
and runInterruptible
are part of Coroutines.
At the moment of writing this post support of
suspend
functions in KMM was experimental.
We will need a class:
data class Event(
val id: UUID,
val value: Int,
val message: String?,
var badModifyablePropertyForDemoPurposes = "Some string"
)
run
is a scope function (but can run without an object). This means you can call it on an object and code block will have direct access to object’s properties and methods without this
(but you can use this
too). Also run
can return a result which can be used in subsequent steps.
val event = Event(
id = UUID.randomUUID(),
value = 10,
message = null
)val isEven = event
.run {
value % 2 == 0
}
println("Is Event.value even? $isEven.")
Prints
Is Event.value even? true.
run
can modify the original object.
val event = Event(
id = UUID.randomUUID(),
value = 10,
message = null,
badModifyablePropertyForDemoPurposes = "Some string"
)event.run {
badModifyablePropertyForDemoPurposes = "Hello"
}
Event(..., badModifyablePropertyForDemoPurposes=Hello)
So, what sets run
apart from apply
? Well, the main difference is in their return values. run
is flexible. It can return any type, not only the type of the object it’s called on. apply
, on the other hand, always returns the object itself, which is excellent for chaining object configurations.
Also, as mentioned earlier, run
can operate independently of an object. This is in contrast to apply
, which always requires an object to work with.
val event = Event(
id = UUID.randomUUID(),
value = 10,
message = null
)event.message?.let {
println("The message is $it")
} ?: run {
println("The message is null")
}
run
is used as a fallback for the case when event.message
is null.
run
quite handy, especially when combined with other scope functions while aiming to maintain consistent code architecture. For safety, it’s ideal to ensure the code within a run
block is less prone to throwing exceptions. However, for situations where exception handling is necessary, runCatching
is the better choice.
This is a variation of run
. runCatching
is literally try…catch
block but with important difference. It encapsulates the result of the block execution into a Result
object. This encapsulation not only makes the code more readable but also facilitates safe data retrieval. An added advantage is that results of runCatching
blocks exectution can be compared.
data class Event(
val id: UUID,
val value: Int,
val message: String?,
var badModifyablePropertyForDemoPurposes: String
)val event = Event(
id = UUID.randomUUID(),
value = 10,
message = null,
badModifyablePropertyForDemoPurposes = "Some string"
)
val result = event.runCatching {
value / 0
}.onFailure {
println("We failed to divide by zero. Throwable: $it")
}.onSuccess {
println("Devision result is $it")
}
println("Returned value is $result")
Prints
18:01:58.722 I We failed to divide by zero. Throwable: java.lang.ArithmeticException: divide by zero
18:01:58.723 I Returned value is: Failure(java.lang.ArithmeticException: divide by zero)
So, as you see using runCatching
offers several advantages. The result of the block execution can be consumed in a chainable manner or returned into a variable and processed later, for example, emitted in flow.
Result
class provides many useful methods and properties to work with holding value. What is more interesting is that you can extend its methods and add more sophisticated logic to exception handling.
The only common ground between runBlocking
, runInterruptible
, and the synchronous run
and runCatching
is their ability to execute a block of code. However, runBlocking
and runInterruptible
differ significantly not only from their namesakes run
and runCatching
but also from each other in terms of functionality and use cases.
For the demo, we’ll be using the FlowGenerator
class that I used in my series of articles about Kotlin flows
class EventGenerator {
/**
* Simulates a stream of data from a backend.
*/
val coldFlow = flow {
val id = UUID.randomUUID().toString()// Simulate a stream of data from a backend
generateSequence(0) { it + 1 }.forEach { value ->
delay(1000)
println("Emitting $value")
emit(value)
}
}
}
This class provides a single instance of infinite cold flow with a suspension point (delay
). This flow is suspendable and cancellable. It follows coroutine rules and controls.
Also it represents async flow that never ends, what we actually should expect from any flow. It helps understand asynchronous sand parallel execution problems better.
Primary use cases are (once again):
The main question is why only these?
Why does this function need to be avoided as I see in replies on StackOverflow, for example. Yes, it blocks the current thread, but we can spawn our own thread and it will not affect other code.
Let’s try.
private fun runFlows() {
thread {
runCollection()
}
}private fun runCollection() {
runBlocking {
val eventGenerator = EventGenerator()
eventGenerator
.coldFlow
.take(2)
.collect {
println("Collection in runCollections #1: $it")
}
}
CoroutineScope(Dispatchers.Default).launch {
runBlocking {
val eventGenerator = EventGenerator()
eventGenerator.coldFlow.collect {
println("Collection in runCollections #2: $it")
}
}
}
}
In this example, I’ve intentionally called runBlocking
from within a coroutine. Despite the documentation advising against this practice, doing so doesn’t trigger any warnings or errors in the IDE, build log, or at runtime.
This means that identifying and tracking such usage falls entirely on you, the developer.
Direct insertions of runBlocking
are relatively easy to spot and fix. However, imagine a scenario where runBlocking
is hidden behind a function call from a library or other module and cannot be spotted easily. The behavior remains the same, but debugging turns into a nightmare.
The code prints
18:24:28.091 I Emitting 0
18:24:28.096 I Collection in runCollections #1: 0
18:24:29.099 I Emitting 1
18:24:29.099 I Collection in runCollections #1: 1
18:24:30.102 I Emitting 2
18:24:30.102 I Collection in runCollections #1: 2
18:24:31.103 I Emitting 3
18:24:31.103 I Collection in runCollections #1: 3
18:24:32.105 I Emitting 4
18:24:32.105 I Collection in runCollections #1: 4
As you see there is no “Collection in runCollections #2” in the log. The reason is that flow is infinite and will never end. The thread stays locked forever.
In real life, you might have a long network or database operation. Running it in runBlocking will severely affect an app performance… or library performance. Try to debug it in lib…
If the flow is finite then collection in coroutine will start, but in normal asynchronous code, next operation should not wait. It’s potential performance degradation. Except in case you really need to do something before the rest of async code starts. It could be, as mentioned in docs — external library handling.
Let’s modify code
private fun runFlows() {
thread(name = "Manual Thread") {
runCollection()
}}
private fun runCollection() {
val coroutine1 = CoroutineScope(Dispatchers.Default).launch {
runBlocking {
val eventGenerator = EventGenerator()
eventGenerator
.coldFlow
.collect {
println("Collection in runCollections #1: $it")
}
}
}
val coroutine2 = CoroutineScope(Dispatchers.Default).launch {
runBlocking {
val eventGenerator = EventGenerator()
eventGenerator.coldFlow.collect {
println("Collection in runCollections #2: $it")
}
}
}
}
Prints
21:33:38.848 I Emitting 0
21:33:38.851 I Collection in runCollections #1: 0
21:33:38.867 I Emitting 0
21:33:38.867 I Collection in runCollections #2: 0
21:33:39.852 I Collection in runCollections #1: 1
21:33:39.876 I Collection in runCollections #2: 1
21:33:40.854 I Emitting 2
21:33:40.854 I Collection in runCollections #1: 2
21:33:40.879 I Emitting 2
21:33:40.879 I Collection in runCollections #2: 2
Everything looks OK by the log, both coroutines are running. This is because CoroutineScope(Dispatchers.Default).launch
selects a thread for the coroutine, thereby mitigating the negative impact of a thread being locked by runBlocking
.
This thread management mitigates the issue with blocked coroutines, ensuring smoother execution even when runBlocking
is used within a coroutine context.
1. runFlows
+- thread
+- Thread[Manual Thread,5,main]
2. runFlows
+- thread
+- runCollections
+- coroutine1
+- Thread[DefaultDispatcher-worker-3,5,main]
3. runFlows
+- thread
+- runCollections
+- coroutine1
+- Thread[DefaultDispatcher-worker-2,5,main]
Everything seems to be working: the application doesn’t crash, and performance is moderate. However, this approach raises a question about its practicality. Here app spawns a coroutine, which in turn spawns a thread, only to then call runBlocking
which creates another coroutine, and to get exactly the same behavior with regular use of coroutines.
This method contradicts the very principles of efficient and predictable code. It disrupts the logical flow and makes it challenging to predict the long-term implications on the application’s performance and behavior. If you encounter such a pattern in your code, it’s better to fix the code as soon as possible.
Now, let’s take a look on a more realistic scenario, with use of a viewModel.
class MainViewModel : ViewModel() {
fun runFlows() {
thread(
name = "Manual Thread",
) {
println("Thread: ${Thread.currentThread()}")
runCollection()
}}
private suspend fun collect(action: (Int) -> Unit) {
runBlocking {
val eventGenerator = EventGenerator()
eventGenerator
.coldFlow
.collect {
action(it)
}
}
}
private fun runCollection() {
viewModelScope.launch {
collect {
println("Collection in runCollections #1: $it: ${Thread.currentThread()}")
}
}
viewModelScope.launch {
collect {
println("Collection in runCollections #2: $it: ${Thread.currentThread()}")
}
}
}
}
Prints
00:40:44.332 I Emitting 0
00:40:44.334 I Collection in runCollections #1: 0: Thread[main,5,main]
00:40:45.336 I Emitting 1
00:40:45.336 I Collection in runCollections #1: 1: Thread[main,5,main]
00:40:46.337 I Emitting 2
00:40:46.338 I Collection in runCollections #1: 2: Thread[main,5,main]
Pay attention that the spawn thread gives nothing it just spawns a thread which does not affect async operations at all. viewModelScope
is bound to the main dispatcher which in the end comes to the main thread (This is a simplified explanation, of course, as digging into the details of dispatchers and the distinctions between Main
and Main.immediate
is off this article).
If runBlocking
removed from the collect()
implementation then call to runFlows()
prints
01:05:48.180 I Emitting 0
01:05:48.181 I Collection in runCollections #1: 0: Thread[main,5,main]
01:05:48.181 I Emitting 0
01:05:48.181 I Collection in runCollections #2: 0: Thread[main,5,main]
01:05:49.182 I Emitting 1
01:05:49.182 I Collection in runCollections #1: 1: Thread[main,5,main]
01:05:49.183 I Emitting 1
01:05:49.183 I Collection in runCollections #2: 1: Thread[main,5,main]
Which is what we normally expect from async operations. Yes, expected, but not obvious if you do not keep in mind what viewModelScope
is bound to.
Moving thread
to collect()
function
private suspend fun collect(action: (Int) -> Unit) {
thread(
name = "Manual Thread",
) {
runBlocking {
val eventGenerator = EventGenerator()
eventGenerator
.coldFlow
.collect {
action(it)
}
}
}
}
also gives a similar result
01:08:51.944 I Emitting 0
01:08:51.944 I Emitting 0
01:08:51.946 I Collection in runCollections #2: 0: Thread[Manual Thread,5,main]
01:08:51.947 I Collection in runCollections #1: 0: Thread[Manual Thread,5,main]
01:08:52.948 I Emitting 1
01:08:52.948 I Emitting 1
01:08:52.948 I Collection in runCollections #1: 1: Thread[Manual Thread,5,main]
01:08:52.948 I Collection in runCollections #2: 1: Thread[Manual Thread,5,main]
But… definitely, you should clearly understand what is happening with such constructions. Using runBlocking
you easily lose track of async operations and lose powerful features of coroutines for automated management of suspension and switching coroutines to perform. Not the best thing if you are not an expert in Java and threads on Android and for some reason coroutines implementation does not fit your needs.
In other cases, limit the use of runBlocked
to what documentation is defined. It feels that at least in mobile app development it should be used mostly in tests.
The final one. No it is not a counterpart for runBlocking
🙂
The documentation states that a block of code will be called in an interruptible manner. This function does not spawn threads and follow the dispatcher you supply as a parameter.
I added new methods into the viewModel.
fun runInterruptible() {
viewModelScope.launch {
println("Start")kotlin.runCatching {
withTimeout(100) {
runInterruptible(Dispatchers.IO) {
interruptibleBlockingCall()
}
}
}.onFailure {
println("Caught exception: $it")
}
println("End")
}
}
private fun interruptibleBlockingCall() {
Thread.sleep(3000)
}
Prints
11:06:29.259 I Start
11:06:30.431 I Caught exception: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 100 ms
11:06:30.431 I End
Note the chain of calls. runCatching
(could be try…catch
) then withTimeout
. I’m using Kotlin 1.9.20, and withTimeout
throws exception but I do not see it log. If I add try…catch
or runCatching
then I can retrieve exception, without it — coroutine stops working but silently.
I didn’t find reason of this behavior and I see no reports in tracker. So keep in mind to use
try…catch
orwithTimeoutOrNull
.
I expected that this function would be just a boring function. However, it turned out to be a very puzzling function. To make it work, you need to clearly understand what code you are running is doing and how it is implemented. Yes, as usual, but here you need to know if any of parts is not cancellable or does not handle thread cancellation. This is tricky.
Tags In
Related Posts
Leave a Reply Cancel reply
Categories
- ! Без рубрики (1)
- ++PU (1)
- 1 (1)
- 1w (1)
- 1win Brazil (1)
- 1win India (1)
- 1WIN Official In Russia (1)
- 1win Turkiye (1)
- 1xbet egypt (1)
- 2ankarafayansustasi.net_may (1)
- ankarafayansustasi.netsiteai apr (1)
- Artificial intelligence (1)
- Arts & Entertainment, Photography (1)
- Banda (5)
- belugasitesi_mAY (1)
- BH_TOPsitesi apr (1)
- BHsitesy_may (2)
- Blog (3)
- Bookkeeping (14)
- Bootcamp de programação (2)
- Bootcamp de programación (2)
- BT_TOPsitesi apr (1)
- casino (5)
- casinom-hub (1)
- casinom-hub.comsitesi apr (3)
- colombian mail order brides (1)
- Cryptocurrency exchange (2)
- Dinamobet_next (1)
- Disease & Illness, Colon Cancer (1)
- Dumanbet (1)
- Dumanbet_next (1)
- Finance, Insurance (1)
- FinTech (5)
- Forex Trading (11)
- Galabet (1)
- Health & Fitness, Fitness Equipment (1)
- Hitbet (1)
- Home & Family, Crafts (1)
- Home & Family, Gardening (1)
- Internet Business, Audio-Video Streaming (1)
- Internet Business, Ecommerce (1)
- Internet Business, Email Marketing (1)
- Internet Business, Internet Marketing (1)
- IT Вакансії (1)
- IT Образование (5)
- IT Освіта (1)
- latin women dating (1)
- mail order bride (1)
- Mars bahis (2)
- Matadorbet (1)
- minimiri.comsitesi apr (3)
- Mobile App Development (771)
- Mostbet Russia (1)
- New Post (1)
- News (12)
- PB_TOPsitesi apr (1)
- PBsitesi_may (1)
- Pusulabet (1)
- redmirepool.bizsitesi apr (2)
- redmirepoolsitesi_may (1)
- Reference & Education, College (1)
- Reference & Education, Sociology (1)
- Rokusitesi apr (1)
- Sober living (6)
- Society, Divorce (1)
- Software development (7)
- Superbetin (1)
- Tempobet_next (1)
- thelongeststride.comsitesi apr (1)
- tipobet-turkiyesitesi apr (1)
- Ultrabet (1)
- Uncategorized (1)
- Игра (2)
- казино (1)
- Криптовалюты (1)
- Новости Криптовалют (1)
- Финтех (7)
- Форекс Брокеры (9)
- Форекс обучение (2)