Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix runBlocking in coroutines #4340

Merged
merged 6 commits into from
Apr 20, 2024

Conversation

bbrockbernd
Copy link
Contributor

Summary

I found a few occasions where the runBlocking coroutine builder is called from within other coroutines. This can affect performance since it blocks the thread that is shared among coroutines, and can in some cases lead to the infamous nested runBlocking deadlock. Often an easy fix is to turn the containing function into a suspend function.

Let me know if I missed something!
Cheers

Screenshots

Link to pull request in Documentation repository

Any other notes

Copy link

@home-assistant home-assistant bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @bbrockbernd

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant home-assistant bot marked this pull request as draft April 10, 2024 09:59
@home-assistant
Copy link

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@bbrockbernd bbrockbernd marked this pull request as ready for review April 10, 2024 10:09
@bbrockbernd bbrockbernd changed the title Fix run blockings Fix runBlocking in coroutines Apr 10, 2024
@bbrockbernd bbrockbernd marked this pull request as draft April 10, 2024 10:32
@bbrockbernd bbrockbernd marked this pull request as ready for review April 10, 2024 11:39
@jpelgrom
Copy link
Member

can in some cases lead to the infamous nested runBlocking deadlock. Often an easy fix is to turn the containing function into a suspend function.

Forgive me for asking what could be an obvious question, but can you link to an example or more specific documentation about this? I understand avoiding the use of runBlocking in suspending functions if possible, but some of these changes (especially in ThemesManager) look like moving around runBlocking { ... } for little to no reason to me at first glance.

@bbrockbernd
Copy link
Contributor Author

Definitely! The documentation states that blocking a coroutine "potentially leads to thread starvation issues". (last sentence of https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/run-blocking.html). Some intuition of why this can be dangerous: https://betterprogramming.pub/how-i-fell-in-kotlins-runblocking-deadlock-trap-and-how-you-can-avoid-it-db9e7c4909f1. In the end of the day, you want to avoid calling runBlocking from coroutines as much as possible.

For instance, the LanguagesManager.saveLang(lang: String?) function calls runBlocking

fun saveLang(lang: String?) {
return runBlocking {

However this function is called from SettingsPresenterImpl.putString(key: String, value: String?) from within a coroutine launched on the Main dispatcher. And thus blocking the UI thread.
override fun putString(key: String, value: String?) {
mainScope.launch {
when (key) {
"themes" -> themesManager.saveTheme(value)
"languages" -> langsManager.saveLang(value)

Since this is the only call site for saveLang we can easily turn this function into a suspend function and get rid of the runBlocking builder.

Now in the case of ThemesManager the getCurrentTheme() function which contains a runBlocking builder, is called from SettingsPresenterImpl.getString which contains a runBlocking as well. The issue here is that the getCurrentTheme() function is also called from synchronous code. To solve this, you can turn this function into a suspend function and move the runBlocking to the synchronous call sites (in this case ThemesManager.setThemeForWebView and ChangeLog.showChangeLog)

@jpelgrom
Copy link
Member

Thanks for the specific example and explanation, that makes it easier to follow :) These changes look good to me.

There are also other places where runBlocking is used - have you checked all or are these just a few that you immediately noticed? I think almost all others are called from normal Android functions or other places where it is not nested in a suspending function, but would appreciate a second opinion.

@bbrockbernd
Copy link
Contributor Author

No problem! As a matter of fact we are developing a tool that should be able to detect these runBlockings and are currently testing it on open source projects! It found actually one more occurence:

io.homeassistant.companion.android.common.data.servers.ServerManagerImpl.removeServer
io.homeassistant.companion.android.common.data.servers.ServerManagerImpl.integrationRepository
io.homeassistant.companion.android.common.data.servers.ServerManagerImpl.activeServerId
--> runBlocking

However, I did not see a quick solution to solve this one.. The longer the callstack between runBlockings gets the trickier.

@jpelgrom
Copy link
Member

Yes that one will be tricky so let's not touch it for now and do this PR first.

@dshokouhi dshokouhi merged commit a363039 into home-assistant:master Apr 20, 2024
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants