On October 29, 2018 JetBrains released a new version of Kotlin (1.3) that included a long-awaited feature of the language: coroutines. Contrary to Go’s goroutine
construct, they are not implemented as a language feature. Instead they are developed as a pure library implementation written in Kotlin. But apart from that they share a lot of similiarites with goroutines.
Behind the scenes, coroutines are very lightweight because they are being multiplexed on a few threads and creating one is much faster than spawning a new thread.
You can learn more about them over at JetBrains’ tutorial: “Your first coroutine with Kotlin”. Here I want to show you the caveat that you might encounter in a JavaFX applications when using coroutines.
So this is my basic JavaFX application written in Kotlin:
class MyApp : Application() {
private val button = Label().also { it.text = "waiting for 0s" }
override fun start(primaryStage: Stage?) {
val pane = Pane().also {
it.children.add(button)
}
primaryStage?.scene = Scene(pane,200.0,200.0)
primaryStage?.show()
}
}
fun main() {
launch(MyApp::class.java)
}
Nothing special here, just a GUI with a label that shows the text waiting for 0s
. Now we want to extend our application to continuously update the label to always tell me the exact time I waited.
class MillisElapsedCounter(private val observer: Observer) {
fun start() {
GlobalScope.launch(Dispatchers.Default) {
var millisElapsed = 0
while (true) {
delay(1000)
millisElapsed += 1000
observer.notify(millisElapsed)
}
}
}
interface Observer {
fun notify(millisElapsed: Int)
}
}
Notice that you need to add a dependency to org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1
to be able to run this (remember: it’s just a library!)
So this is the implemention of the MillisElapsedCounter
class and a corresponding Observer
that is being notified every 1000ms. Clients of this class have to instantiate it providing an observer. Calling start()
will then launch a new coroutine using the default dispatcher (there is a pool of dispatchers - which are threads behind the scenes - you can use) and return immediately. Note that delay(1000)
is a suspend
function, it will suspend execution in the thread it runs in. Running that in the application thread would freeze the UI forever and would be a bad idea.
To use the counter we also need to update MyApp
to implement MillisElapsedCounter.Observer
’s notify(millisElapsed: Int)
and start the counter like this:
class MyApp : Application(), MillisElapsedCounter.Observer {
private val button = Label().also { it.text = "waiting for 0s" }
override fun start(primaryStage: Stage?) {
MillisElapsedCounter(this).start()
...
}
override fun notify(millisElapsed: Int) {
button.text = "waiting for ${millisElapsed / 1000}s"
}
}
Now if you run that, it will throw a java.lang.IllegalStateException
exception that is coming from JavaFX:
Exception in thread “DefaultDispatcher-worker-3” java.lang.IllegalStateException: Not on FX application thread; currentThread = DefaultDispatcher-worker-3
To prevent from concurrency issues in the state of the UI, JavaFX prevents other threads from changing it. Look at the main()
function that uses launch(MyApp::class.java)
to start the application. This starts a new thread called JavaFX-Launcher
which in turn starts the JavaFX Application Thread
which runs all the UI related stuff. This one is the only thread allowed to make changes in the UI at runtime.
This means that we need to execute the changes that are being made to button.text
in this very thread. The Kotlin team implemented another library for this sole purpose: org.jetbrains.kotlinx:kotlinx-coroutines-javafx:1.1.1 provides a new Dispatcher Dispatchers.JavaFx
that uses the JavaFX Application Thread (there’s also -android and -swing to be used correspondingly).
Then the solution to our problem here is to use the correct Dispatcher Dispatchers.JavaFx
and to implement another interface in MyApp
called CoroutineScope
:
class MyApp : Application(), MillisElapsedCounter.Observer, CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.JavaFx
...
As it is now a CoroutineScope
we can launch new coroutines directly from notify()
using the correct scope:
override fun notify(millisElapsed: Int) {
launch {
button.text = "waiting for ${millisElapsed / 1000}s"
}
}
And that’s it! The exception will no longer occur and everything will run as expected - I waited a long time, and it still counts happily the seconds I waited :)
One thing we can improve though is that MillisElapsedCounter
runs a coroutine in the GlobalScope
which means that you need to keep track of it’s lifetime (if you were interested in that - eg. stopping it). More details on that in The reason to avoid GlobalScope. To fix that we could implement the interface CoroutineScope
in MillisElapsedCounter
as well:
class MillisElapsedCounter(private val observer: Observer) : CoroutineScope {
override val coroutineContext: CoroutineContext
get() = Dispatchers.Default
fun start() {
launch { ... }
}
...
We now have two coroutines cleanly separated from each other and each running in it’s own scope. A clean and simple solution I think.
The complete source code is available on GitHub, so you can clone it and play around with it:
domnikl / kotlin-coroutines-and-javafx-threads
Sample code from my blog post
You can read more about that in Guide to UI programming with coroutines.
Top comments (0)