Thread-specific components as a type

In user interface programming most of the commonly used user interface toolkits work on a single-threaded model. In this post we will look at an example in ScalaFX, but the same principle applies to JavaFX and Swing. In these toolkits, a component must only be accessed from the dedicated event processing thread (e.g Platform FX Application Thread) after it has been realized, except when using methods documented as thread-safe. A component is realized when it is made ready to be painted, e.g. by calling pack(), show() or setVisible(true) either on the component or on one of its ancestors in the component hierarchy. In practice this means that components can be constructed and initialized on other threads, but as soon as the component has been realized, access to the component must be confined to the limited number of thread-safe methods or to methods running on the event processing thread.

The rules described above are well-documented but can be hard to follow in practice. This is where the type system can help. The book Functional Programming in Scala:
Functional Programming in Scala - front cover explains how to design and build type algebras using increasingly advanced concepts, however the design presented here is a very simple application of the techniques described in the book.

Our type algebra needs to represent the idea of a component that can only be accessed from the event processing thread.


class Confined[T](t: T) {}

T is the generic type of the component. We could restrict this to be a subclass of a component (e.g. T <: scalafx.scene.Node), but for now we will leave T unbounded. We want to put the component into a context that controls access to its public API:


object Confined { def apply[T](t: T): Confined[T] = new Confined[T](t) }

We also want to be able to use the component’s API from our own code:


class Confined[T](t: T) { def run(f: T => Unit): Unit = ??? }

… but we need to ensure that our code is run on the event processing thread:


def run(f: T => Unit): Unit = { if (Platform.isFxApplicationThread) f(t) else Platform.runLater(f(t)) }

We could go further and start to wonder if this class fits one of the standard functional programming patterns, such as Monoid or Monad, but at first sight the Unit return type of our function and the possibility that we will switch threads inside the run(…) function makes it appear that our class does not neatly fit into either of these patterns.

Let’s leave it there for now and add some Scaladoc to our completed code:


package com.mouseforge.scalafx import scalafx.application.Platform /** * Confined class helper methods. */ object Confined { /** * Create a new Confined instance of generic type T. * * @param t the instance to be confined to the Platform FX Application Thread. * @tparam T the type of instance t. * @return a new Confined instance encapsulating the instance that should be confined to the Platform FX Application Thread. */ def apply[T](t: T): Confined[T] = new Confined[T](t) } /** * Encapsulates a user interface component that should be confined to the Platform FX Application Thread. * * A function on a confined instance will only be performed immediately if called from the Platform FX Application Thread, * otherwise the function will be scheduled to run later on the Platform FX Application Thread. * * Uses the idea of a type algebra described in Functional Programming in Scala. * * @param t the instance that should only be accessed on the Platform FX Application Thread. * @tparam T the type of the instance t. */ class Confined[T](t: T) { /** * Perform a function on the operand on the Platform FX Application Thread. * * @param f the function to perform on the operand to be performed on the Platform FX Application Thread. */ def run(f: T => Unit): Unit = { if (Platform.isFxApplicationThread) f(t) else Platform.runLater(f(t)) } }

Now we can use the type system to protect our user interface component from accidental access from the wrong thread like this:


stage.show() // Stage and all its components are now realized - confine the sceneRef. val confinedScene = Confined(sceneRef) // We want to add a ticker-tape style label - build the confined label val confinedLabel: Confined[Label] = Confined(new Label {text = ""; textFill = Color.DarkOrchid; font = Font(24.0)}) // Add the label to the scene using the confined elements. confinedScene.run(cs => confinedLabel.run(label => cs.content.add(label))) // Start the ticker-tape message. new Thread(() => adjustText(confinedLabel, "Hello from Mouseforge!!!")).start()

If the components are confined then it is very difficult to accidentally access them except from the event processing thread. It is still possible to extract the component but it is hard to do accidentally:


// Bad code var unsafeLabel = _ confinedLabel.run(cl => unsafeLabel = cl) // Don't do this! confinedLabel.run(cl => unsafeLabel.synchronized { unsafeLabel = cl // Thread-safe, but don't do this either. })

It is also possible to access the unconfined components while they are still in scope. In general, this technique isn’t foolproof, but I have found it useful to ensure my user interface updates are not accidentally processed on my background threads. The method that catches me out most often is size(), it’s very easy to call on the wrong thread and hard to spot once you’ve done it.

This is the full ticker-tape example:


package com.mouseforge.scalafx import java.io.InputStream import java.net.URL import scalafx.application.JFXApp import scalafx.scene.Scene import scalafx.scene.image.{Image, ImageView} import scalafx.scene.control.Label import scalafx.scene.layout.StackPane import scalafx.scene.paint.Color import scalafx.scene.text.Font object HelloMouseforge extends JFXApp { val sceneRef: Scene = new Scene { root = new StackPane() } stage = new JFXApp.PrimaryStage { title.value = "Mouseforge" width = 377 height = 324 var inputStream: InputStream = _ try { inputStream = new URL("https://mouseforge.com/wp-content/uploads/2015/04/mouseforge.jpg").openStream() val image = new Image(inputStream) val imageView = new ImageView(image) sceneRef.content.add(imageView) scene = sceneRef } finally { inputStream.close() } } def adjustText(confinedTextBox: Confined[Label], fullText: String): Unit = { for (loop <- 0 to 1000) { val text = fullText.take(loop % (fullText.length + 1)) confinedTextBox.run(l => l.text = text) Thread.sleep(125L) } } stage.show() // Stage and all its components are now realized - confine the sceneRef. val confinedScene = Confined(sceneRef) // We want to add a ticker-tape style label - build the confined label val confinedLabel: Confined[Label] = Confined(new Label {text = ""; textFill = Color.DarkOrchid; font = Font(24.0)}) // Add the label to the scene using the confined elements. confinedScene.run(cs => confinedLabel.run(label => cs.content.add(label))) // Start the ticker-tape message. new Thread(() => adjustText(confinedLabel, "Hello from Mouseforge!!!")).start() } import scalafx.application.Platform /** * Confined class helper methods. */ object Confined { /** * Create a new Confined instance of generic type T. * * @param t the instance to be confined to the Platform FX Application Thread. * @tparam T the type of instance t. * @return a new Confined instance encapsulating the instance that should be confined to the Platform FX Application Thread. */ def apply[T](t: T): Confined[T] = new Confined[T](t) } /** * Encapsulates a user interface component that should be confined to the Platform FX Application Thread. * * A function on a confined instance will only be performed immediately if called from the Platform FX Application Thread, * otherwise the function will be scheduled to run later on the Platform FX Application Thread. * * Uses the idea of a type algebra described in Functional Programming in Scala. * * @param t the instance that should only be accessed on the Platform FX Application Thread. * @tparam T the type of the instance t. */ class Confined[T](t: T) { /** * Perform a function on the operand on the Platform FX Application Thread. * * @param f the function to perform on the operand to be performed on the Platform FX Application Thread. */ def run(f: T => Unit): Unit = { if (Platform.isFxApplicationThread) f(t) else Platform.runLater(f(t)) } }

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.