actorconcurrent.futuresscalafxexecutioncontext

How can I write a save GUI-Aktor for Scalafx?


Basically I want an Aktor to change a scalafx-GUI safely.

I've read many posts describing this, but there where sometimes contradictory and some years old, so some of them might be outdated. I have a working example code and I basically want to know if this kind of programming is thread-save. The other question is if I can configure sbt or the compiler or something in a way, that all threads (from the gui, the actors and the futures) are started by the same dispatcher.

I've found some example code "scalafx-akka-demo" on GitHub, which is 4 years old. What I did in the following example is basically the same, just a little simplified to keep things easy.

Then there is the scalatrix-example approximately with the same age. This example really worries me. In there is a self-written dispatcher from Viktor Klang from 2012, and I have no idea how to make this work or if I really need it. The question is: Is this dispatcher only an optimisation or do I have to use something like it to be thread save?

But even if I don't absolutely need the dispatcher like in scalatrix, it is not optimal to have a dispatcher for the aktor-threads and one for the scalafx-event-threads. (And maybe one for the Futures-threads as well?)

In my actual project, I have some measurement values coming from a device over TCP-IP, going to an TCP-IP actor and are to be displayed in a scalafx-GUI. But this is much to long.

So here is my example code:

import akka.actor.{Actor, ActorRef, ActorSystem, Props}
import scala.concurrent.{Await, Future}
import scala.concurrent.duration._
import scalafx.Includes._
import scalafx.application.{JFXApp, Platform}
import scalafx.application.JFXApp.PrimaryStage
import scalafx.event.ActionEvent
import scalafx.scene.Scene
import scalafx.scene.control.Button
import scalafx.stage.WindowEvent
import scala.concurrent.ExecutionContext.Implicits.global

object Main extends JFXApp {
  case object Count
  case object StopCounter
  case object CounterReset

  val aktorSystem: ActorSystem = ActorSystem("My-Aktor-system") // Create actor context
  val guiActor: ActorRef = aktorSystem.actorOf(Props(new GUIActor), "guiActor") // Create GUI actor

  val button: Button = new Button(text = "0") {
    onAction = (_: ActionEvent) => guiActor ! Count
  }

  val someComputation = Future {
    Thread.sleep(10000)
    println("Doing counter reset")
    guiActor ! CounterReset
    Platform.runLater(button.text = "0")
  }

  class GUIActor extends Actor {
    def receive: Receive = counter(1)

    def counter(n: Int): Receive = {
      case Count        =>
        Platform.runLater(button.text = n.toString)
        println("The count is: " + n)
        context.become(counter(n + 1))
      case CounterReset => context.become(counter(1))
      case StopCounter  => context.system.terminate()
    }
  }
    
  stage = new PrimaryStage {
    scene = new Scene {
      root = button
    }
    onCloseRequest = (_: WindowEvent) => {
      guiActor ! StopCounter
      Await.ready(aktorSystem.whenTerminated, 5.seconds)
      Platform.exit()
    }
  }
}

So this code brings up a button, and every time it is clicked the number of the button increases. After some time the number on the button is reset once.

In this example-code I tried to bring the scalafx-GUI, the actor and the Future to influence each other. So the button click sends a message to the actor, and then the actor changes the gui - which is what I am testing here. The Future also sends to the actor and changes the gui.

So far, this example works and I haven't found everything wrong with it. But unfortunately, when it comes to thread-safety this doesn't mean much

My concrete questions are:

  1. Is the method to change the gui in the example code thread save?

  2. Is there may be a better way to do it?

  3. Can the different threads be started from the same dispatcher? (if yes, then how?)


Solution

  • To address your questions:

    1) Is the method to change the gui in the example code thread save?

    Yes.

    JavaFX, which ScalaFX sits upon, implements thread safety by insisting that all GUI interactions take place upon the JavaFX Application Thread (JAT), which is created during JavaFX initialization (ScalaFX takes care of this for you). Any code running on a different thread that interacts with JavaFX/ScalaFX will result in an error.

    You are ensuring that your GUI code executes on the JAT by passing interacting code via the Platform.runLater method, which evaluates its arguments on the JAT. Because arguments are passed by name, they are not evaluated on the calling thread.

    So, as far as JavaFX is concerned, your code is thread safe.

    However, potential issues can still arise if the code you pass to Platform.runLater contains any references to mutable state maintained on other threads.

    You have two calls to Platform.runLater. In the first of these (button.text = "0"), the only mutable state (button.text) belongs to JavaFX, which will be examined and modified on the JAT, so you're good.

    In the second call (button.text = n.toString), you're passing the same JavaFX mutable state (button.text). But you're also passing a reference to n, which belongs to the GUIActor thread. However, this value is immutable, and so there are no threading issues from looking at its value. (The count is maintained by the Akka GUIActor class's context, and the only interactions that change the count come through Akka's message handling mechanism, which is guaranteed to be thread safe.)

    That said, there is one potential issue here: the Future both resets the count (which will occur on the GUIActor thread) as well as setting the text to "0" (which will occur on the JAT). Consequently, it's possible that these two actions will occur in an unexpected order, such as button's text being changed to "0" before the count is actually reset. If this occurs simultaneously with the user clicking the button, you'll get a race condition and it's conceivable that the displayed value may end up not matching the current count.

    2) Is there may be a better way to do it?

    There's always a better way! ;-)

    To be honest, given this small example, there's not a lot of further improvement to be made.

    I would try to keep all of the interaction with the GUI inside either GUIActor, or the Main object to simplify the threading and synchronization issues.

    For example, going back to the last point in the previous answer, rather than have the Future update button.text, it would be better if that was done as part of the CounterReset message handler in GUIActor, which then guarantees that the counter and button appearance are synchronized correctly (or, at least, that they're always updated in the same order), with the displayed value guaranteed to match the count.

    If your GUIActor class is handling a lot of interaction with the GUI, then you could have it execute all of its code on the JAT (I think this was the purpose of Viktor Klang's example), which would simplify a lot of its code. For example, you would not have to call Platform.runLater to interact with the GUI. The downside is that you then cannot perform processing in parallel with the GUI, which might slow down its performance and responsiveness as a result.

    3) Can the different threads be started from the same dispatcher? (if yes, then how?)

    You can specify custom execution contexts for both futures and Akka actors to get better control of their threads and dispatching. However, given Donald Knuth's observation that "premature optimization is the root of all evil", there's no evidence that this would provide you with any benefits whatsoever, and your code would become significantly more complicated as a result.

    As far as I'm aware, you can't change the execution context for JavaFX/ScalaFX, since JAT creation must be finely controlled in order to guarantee thread safety. But I could be wrong.

    In any case, the overhead of having different dispatchers is not going to be high. One of the reasons for using futures and actors is that they will take care of these issues for you by default. Unless you have a good reason to do otherwise, I would use the defaults.