2 votes

Comment puis-je écrire un GUI-Aktor de sauvegarde pour Scalafx ?

En gros, je veux un Aktor pour modifier une interface graphique scalafx en toute sécurité.

J'ai lu de nombreux articles décrivant ce sujet, mais ils sont parfois contradictoires et datent de plusieurs années, donc certains d'entre eux peuvent être dépassés. J'ai un code d'exemple fonctionnel et je veux essentiellement savoir si ce type de programmation permet d'économiser des threads. L'autre question est de savoir si je peux configurer sbt ou le compilateur ou autre de manière à ce que tous les threads (de l'interface, des acteurs et des futurs) soient lancés par le même dispatcher.

J'ai trouvé un exemple de code "scalafx-akka-demo" sur github, qui a 4 ans. Ce que j'ai fait dans l'exemple suivant est fondamentalement le même, juste un peu simplifié pour garder les choses faciles.

Ensuite, il y a l'exemple de scalatrix approximativement avec le même âge. Cet exemple m'inquiète vraiment. Il contient un répartiteur auto-écrit par Viktor Klang en 2012, et je n'ai aucune idée de la façon de le faire fonctionner ou si j'en ai vraiment besoin. La question est la suivante : ce répartiteur est-il seulement une optimisation ou dois-je utiliser quelque chose comme lui pour être thread save ?

Mais même si je n'ai pas absolument besoin du répartiteur comme dans scalatrix, il n'est pas optimal d'avoir un répartiteur pour les aktor-threads et un pour les scalafx-event-threads. (Et peut-être aussi un pour les Futures-threads).

Dans mon projet actuel, j'ai des valeurs de mesure provenant d'un dispositif via TCP-IP, allant vers un acteur TCP-IP et devant être affichées dans une interface utilisateur scalafx. Mais c'est beaucoup trop long.

Voici donc mon exemple de 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()
    }
  }
}

Ce code fait donc apparaître un bouton, et chaque fois que l'on clique dessus, le numéro du bouton augmente. Après un certain temps, le numéro du bouton est réinitialisé une fois.

Dans cet exemple de code, j'ai essayé d'amener le scalafx-GUI, l'acteur et le futur à s'influencer mutuellement. Donc le clic sur le bouton envoie un message à l'acteur, et ensuite l'acteur change le gui - ce qui est ce que je teste ici. Le Future envoie également un message à l'acteur et modifie le gui.

Jusqu'à présent, cet exemple fonctionne et je n'ai rien trouvé à redire. Mais malheureusement, lorsqu'il s'agit d'économiser du fil, cela ne signifie pas grand-chose.

Mes questions concrètes sont les suivantes :

1.) La méthode pour changer le gui dans le code d'exemple est-elle thread save ?

2.) Existe-t-il une meilleure façon de procéder ?

3.) Les différents threads peuvent-ils être lancés à partir du même dispatcher ? (si oui, alors comment ?)

2voto

Mike Allen Points 5616

Pour répondre à vos questions :

1) La méthode pour changer le gui dans le code d'exemple est-elle thread save ?

Oui.

JavaFX qui ScalaFX sur lequel il repose, met en œuvre la sécurité des threads en insistant sur le fait que toutes les GUI Les interactions ont lieu sur le Fil de l'application JavaFX ( JAT ), qui est créé pendant JavaFX initialisation ( ScalaFX s'en charge pour vous). Tout code s'exécutant sur un thread différent qui interagit avec JavaFX / ScalaFX entraînera une erreur.

Vous vous assurez que votre code GUI s'exécute sur l'ordinateur de l'entreprise. JAT en transmettant le code d'interaction via le Platform.runLater qui évalue ses arguments sur le JAT . Parce que les arguments sont passés par nom ils ne sont pas évalués sur le thread appelant.

Donc, en ce qui concerne JavaFX est concerné, votre code est thread safe.

Cependant, des problèmes potentiels peuvent encore survenir si le code que vous transmettez à Platform.runLater contient toute référence à un état mutable maintenu sur d'autres threads.

Vous avez deux appels à Platform.runLater . Dans le premier d'entre eux ( button.text = "0") le seul état mutable ( button.text ) appartient à JavaFX qui sera examiné et modifié sur le site de l JAT donc tu es bon.

Dans le deuxième appel ( button.text = n.toString ), vous passez le même JavaFX état mutable ( button.text ). Mais vous passez également une référence à n qui appartient au GUIActor fil. Cependant, cette valeur est immuable Il n'y a donc pas de problème de threading en regardant sa valeur. (Le compte est maintenu par la fonction Akka GUIActor et les seules interactions qui modifient le compte se font par le biais de Akka Le mécanisme de traitement des messages de l'entreprise, qui est garanti sans risque pour les threads).

Ceci étant dit, il y a un problème potentiel ici : les Future remet à zéro le comptage (ce qui se produira sur le GUIActor ) ainsi que de mettre le texte à "0" (qui aura lieu le JAT ). Par conséquent, il est possible que ces deux actions se produisent dans un ordre inattendu, tel que button Le texte de l'article est modifié comme suit "0" avant le compte est réellement remis à zéro. Si cela se produit en même temps que le clic de l'utilisateur sur le bouton, vous obtiendrez une condition de course et il est concevable que la valeur affichée ne corresponde pas au compte actuel.

2) Existe-t-il une meilleure façon de procéder ?

Il y a toujours un meilleur moyen ! ;-)

Pour être honnête, au vu de ce petit exemple, il n'y a pas beaucoup d'autres améliorations à apporter.

J'essaierais de garder toute l'interaction avec les GUI à l'intérieur de l'un ou l'autre GUIActor ou le Main pour simplifier les questions de threading et de synchronisation.

Par exemple, pour revenir sur le dernier point de la réponse précédente, plutôt que d'avoir l'élément Future mise à jour button.text il serait préférable que cela soit fait dans le cadre de l'enquête sur le marché du travail. CounterReset le gestionnaire de messages dans GUIActor ce qui garantit alors que le compteur et button sont correctement synchronisées (ou, du moins, qu'elles sont toujours mises à jour dans le même ordre), avec la garantie que la valeur affichée correspond au compte.

Si votre GUIActor gère une grande partie de l'interaction avec la classe GUI alors vous pouvez lui faire exécuter tout son code sur le serveur de l'entreprise. JAT (je pense que c'était le but de l'exemple de Viktor Klang), ce qui simplifierait une grande partie de son code. Par exemple, il ne serait pas nécessaire d'appeler Platform.runLater pour interagir avec le GUI . L'inconvénient est qu'il n'est alors pas possible d'effectuer un traitement en parallèle avec la GUI ce qui peut entraîner un ralentissement de ses performances et de sa réactivité.

3) Les différents threads peuvent-ils être lancés à partir du même dispatcher (si oui, comment) ?

Vous pouvez spécifier des contextes d'exécution pour les contrats à terme et les Akka pour obtenir un meilleur contrôle de leurs threads et de leur dispatching. Cependant, compte tenu de l'observation de Donald Knuth selon laquelle "l'optimisation prématurée est la racine de tous les maux", rien ne prouve que cela vous apporterait le moindre avantage, et votre code deviendrait par conséquent beaucoup plus compliqué.

Pour autant que je sache, vous ne pouvez pas changer le contexte d'exécution de la fonction JavaFX / ScalaFX puisque JAT La création doit être finement contrôlée afin de garantir la sécurité du fil. Mais je peux me tromper.

Dans tous les cas, les frais généraux liés à la présence de différents répartiteurs ne seront pas élevés. L'une des raisons d'utiliser les futures et les acteurs est qu'ils prennent en charge ces problèmes par défaut. A moins que vous ayez une bonne raison de faire autrement, j'utiliserais les valeurs par défaut.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X