Kotlinx.coroutines: Annotating off-UI code for flagging at compile / runtime

Created on 2 Oct 2018  路  2Comments  路  Source: Kotlin/kotlinx.coroutines

While I appreciate the compactness of code with some of the coroutines constructs, I still find that it's very easy to run heavy operations on the UI thread. For example, let's say when I click a button in Swing, I want to run some IO to read the disk content and then display the file list in a panel:

button.addActionListener { ev ->
    GlobalScope.launch(Dispatchers.Swing) {
        myPanel.setList(withContext(Dispatchers.Default) {
            FileUtils.getLocalFileSystemContent()
        })
    }
}

Where FileUtils.getLocalFileSystemContent() is a heavy IO-bound operation. If I make a mistake and pass Dispatchers.Swing to that withContext call, that operation runs on the Swing UI thread, with no compile time or run time warning.

It would be great to have some kind of a mechanism to annotate a Java / Kotlin method to only be allowed to run off UI thread (without peppering the explicit checks in those methods themselves), and then detect places that do run such methods on UI-bound context. Ideally at compile time.

question

Most helpful comment

In our team we adopted the following convention: Any public function which needs a specific thread or thread-pool must be marked suspend and use withContext to make sure its content is executed correctly.

Example:

// This function can be called from any thread (including UI)
fun foo() { ... }

// Body of this function may block
suspend fun offUIFunction() = withContext(Dispatchers.IO) { ... }

// Body of this function needs to be executed on Swing
suspend fun uiFunction() = withContext(Dispatchers.Swing) { ... }

This convention brings the following benefits:

  • The Kotlin compiler will fails if one of this function is not invoked from a coroutine
  • The caller of the function does not have to know or care about from which thread a function is supposed to be called.
  • If the function is called from another context, the work will always be dispatched on the correct thread without blocking.
  • If is fast enough, since in the case of which the function is called from the correct context, withContext won't perform unnecessary thread switch.

So in our codebase the code example you provided would like like this:

// Dispatchers.Swing would be the default in Swing views
launch { 

  // we use subscriptions, which helps to prevent memory leaks, since it will be automaticaly removed at the end of the life of the coroutine scope
  button.openActionSubscription().consumeEach { ev ->

    // loadLocalFileSystemContent here would have the responsibility to perform necessary thread switches
    myPanel.setList(loadLocalFileSystemContent()) 
  }
}

All 2 comments

In our team we adopted the following convention: Any public function which needs a specific thread or thread-pool must be marked suspend and use withContext to make sure its content is executed correctly.

Example:

// This function can be called from any thread (including UI)
fun foo() { ... }

// Body of this function may block
suspend fun offUIFunction() = withContext(Dispatchers.IO) { ... }

// Body of this function needs to be executed on Swing
suspend fun uiFunction() = withContext(Dispatchers.Swing) { ... }

This convention brings the following benefits:

  • The Kotlin compiler will fails if one of this function is not invoked from a coroutine
  • The caller of the function does not have to know or care about from which thread a function is supposed to be called.
  • If the function is called from another context, the work will always be dispatched on the correct thread without blocking.
  • If is fast enough, since in the case of which the function is called from the correct context, withContext won't perform unnecessary thread switch.

So in our codebase the code example you provided would like like this:

// Dispatchers.Swing would be the default in Swing views
launch { 

  // we use subscriptions, which helps to prevent memory leaks, since it will be automaticaly removed at the end of the life of the coroutine scope
  button.openActionSubscription().consumeEach { ev ->

    // loadLocalFileSystemContent here would have the responsibility to perform necessary thread switches
    myPanel.setList(loadLocalFileSystemContent()) 
  }
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

elizarov picture elizarov  路  116Comments

elizarov picture elizarov  路  45Comments

elizarov picture elizarov  路  143Comments

NikolayMetchev picture NikolayMetchev  路  46Comments

elizarov picture elizarov  路  35Comments