Scheduler

Sponge dispose d’un Scheduler qui vous permet de désigner des tâches à exécuter plus tard. Le Scheduler fournit un Task.Builder avec lequel vous pouvez spécifier les propriétés de la tâche telles que le délai, l’intervalle, le nom, la/l”(a)synchronicité, et le Runnable (voir Propriétés d’une tâche).

Constructeur de Tâche

Tout d’abord, obtenez une instance de Task.Builder:

import org.spongepowered.api.scheduler.Task;

Task.Builder taskBuilder = Task.builder();

La seule propriété requise est le Runnable, que vous pouvez spécifier en utilisant la méthode Task.Builder#execute(Runnable):

taskBuilder.execute(new Runnable() {
    public void run() {
        logger.info("Yay! Schedulers!");
    }
});

ou en utilisant la syntaxe de Java 8 avec Task.Builder#execute(Runnable runnable)

taskBuilder.execute(
    () -> {
        logger.info("Yay! Schedulers!");
    }
);

ou en utilisant la syntaxe de Java 8 avec Task.Builder#execute(Consumer<Task> task)

taskBuilder.execute(
    task -> {
        logger.info("Yay! Schedulers! :" + task.getName());
    }
);

Propriétés d’une tâche

En utilisant le Task.Builder, vous pouvez spécifier d’autres propriétés optionnelles, comme décrit ci-dessous.

Propriété Méthode Utilisée Description
delay

delayTicks(long delay)

delay(long delay,
TimeUnit unit)

Le montant optionnel de temps avant que la tâche soit exécutée.

Le temps est spécifié en nombre de ticks avec la méthode delayTicks(), ou il peut être communiqué en unité de temps plus commode en spécifiant un TimeUnit avec la méthode delay().

Chacune de ces méthodes, mais pas les deux, peuvent être spécifiées par tâche.

interval
intervalTicks(
long interval)
interval(long interval,
TimeUnit unit)

Le montant de temps entre la répétition de la tâche. Si une intervalle n’est pas spécifiée, la tâche ne sera pas répétée.

Le temps est spécifié en nombre de ticks avec la méthode intervalTicks(), ou il peut être communiqué en unité de temps plus commode en spécifiant un TimeUnit avec la méthode interval().

Chacune de ces méthodes, mais pas les deux, peuvent être spécifiées par tâche.

synchronization async() Une tâche synchrone est lancée dans la boucle principale du jeu en série avec le cycle de tick. Si la méthode Task.Builder#async est utilisée, la tâche sera lancée de manière asynchrone. En conséquence, elle sera effectuée dans son propre thread, indépendamment du cycle de tick, et ne pourrait pas être thread-safe avec les données de jeu. (Voir Tâches asynchrones.)
name name(String name) La nom de la tâche. Par défaut, le nom d’une tâche sera PLUGIN_ID « - » ( « A- » | « S- » ) SERIAL_ID. Par exemple, le nom par défaut d’une tâche pourrait ressembler à « fooplugin-A-12 ». Deux tâches actives ne pourront pas avoir le même serial ID pour le même type de synchronisation. Si un nom de tâche est spécifié, il devrait être descriptif et aider les utilisateurs à déboguer votre plugin.

Enfin, validez la tâche dans le scheduler, en utilisant la méthode Task.Builder#submit(Object).

Et c’est tout! Pour résumer, une tâche planifiée totalement fonctionnelle qui se lancerait toutes les 5 minutes après un délai initial de 100 millisecondes pourrait être construite et validée en utilisant le code suivant:

import java.util.concurrent.TimeUnit;

Task task = Task.builder().execute(() -> logger.info("Yay! Schedulers!"))
    .async().delay(100, TimeUnit.MILLISECONDS).interval(5, TimeUnit.MINUTES)
    .name("ExamplePlugin - Fetch Stats from Database").submit(plugin);

Pour annuler une tâche, utilisez simplement la méthode Task#cancel():

task.cancel();

Si vous devez annuler la tâche à partir de l’exécutable lui-même, vous pouvez plutôt opter pour utiliser un Consumer<Task> afin d’accéder à la tâche. L’exemple ci-dessous va planifier une tâche qui va compter à rebours de 60 et s’annuler lui-même en arrivant à 0.

@Listener
public void onGameInit(GameInitializationEvent event) {
    Task task = Task.builder().execute(new CancellingTimerTask())
        .interval(1, TimeUnit.SECONDS)
        .name("Self-Cancelling Timer Task").submit(plugin);
}

private class CancellingTimerTask implements Consumer<Task> {
    private int seconds = 60;
    @Override
    public void accept(Task task) {
        seconds--;
        Sponge.getServer()
            .getBroadcastChannel()
            .send(Text.of("Remaining Time: "+seconds+"s"));
        if (seconds < 1) {
            task.cancel();
        }
    }
}

Tâches asynchrones

Les tâches asynchrones devraient être principalement utilisées pour un code qui mettrait un temps significatif à s’exécuter, à savoir des requêtes vers un autre serveur ou vers une base de données. Si cela est fait sur le thread principal, une requête vers un autre serveur pourrait grandement impacter les performances du jeu, puisque le prochain tick ne pourra être lancé/exécuté avant que la requête soit finie.

Puisque Minecraft s’exécute dans sa majorité sous un seul thread, il y a peu de choses que vous pouvez faire de manière asynchrone (pour des raisons d’accès concurrentiel). Si vous devez exécuter une tâche asynchrone, vous devriez y exécuter tout le code qui n’utilise pas l’API Sponge ou n’affecte pas Minecraft, puis enregistrer une autre tâche synchrone qui gérera le code nécessitant l’API. Il y a quelque parties de Minecraft avec lesquelles vous pouvez travailler de manière asynchrone, incluant:

  • Chat
  • Gestion des Permissions intégrée à Sponge
  • Scheduler de Sponge

En plus, il y a d’autres opérations qui sont sûres pour être faites de manière asynchrone:

  • Requêtes de réseaux indépendants
  • Système de fichier Entrée/Sortie (sauf les fichiers utilisés par Sponge)

Avertissement

Accéder aux objets du jeu en dehors du thread principal peut causer des crashs, des inconsistances et d’autres problèmes qu’il serait préférable d’éviter. Si cela est mal fait, vous pouvez rencontrer une ConcurrentModificationException avec ou sans crash du serveur au mieux, ou d’une potentielle corruption de joueur/monde/serveur au pire.

Compatibilité avec les autres librairies

Pour que votre plugin augmente en taille et en possibilité, vous pouvez commencer à utiliser une des nombreuse librairies en concurrences, valable pour Java et sa JVM. Ces librairies ont tendances à s’appuyer sur l”ExecutorService de Java afin de mieux diriger sur quel Thread il faut exécuter la tâche correspondante.

Pour permettre à ces librairies de fonctionner avec le Scheduler de Sponge la méthode suivante peut être utilisée:

  • Scheduler#createSyncExecutor(Object) crée un :javadoc:`SpongeExecutorService`qui exécute les tâches en passant par le scheduler synchrone de Sponge.
  • Scheduler#createAsyncExecutor(Object) crée un SpongeExecutorService qui exécute les tâches en passant par le scheduler asynchrone de Sponge. Les tâches sont sujettes aux restrictions mentionnées dans`Tâches asynchrones`_.

Une chose à garder à l’esprit est que chaque tâche qui interagit avec Sponge en dehors des interactions listées dans Tâches Asynchrones doit être exécuté sur le ExecutorService crée avec Scheduler#createSyncExecutor(Object) pour être thread-safe.

import org.spongepowered.api.scheduler.SpongeExecutorService;

SpongeExecutorService minecraftExecutor = Sponge.getScheduler().createSyncExecutor(plugin);

minecraftExecutor.submit(() -> { ... });

minecraftExecutor.schedule(() -> { ... }, 10, TimeUnit.SECONDS);

Presque toutes les librairies ont une manière de s’adapter à l”ExecutorService pour planifier de manière native les tâches. À titre d’exemple les paragraphe suivants vont expliquer comment l”ExecutorService est utilisé dans bon nombre de librairies.

CompletableFuture (Java 8)

Avec Java 8 l’objet CompletableFuture a été ajouté à la library standard. Comparé à l’objet Future, celui-ci permet au développeur de fournir un callback appelé quand le futur se complète plutôt que de bloquer le thread jusqu’à ce que le future se réalise éventuellement.

CompletableFuture est une interface qui possède généralement trois variations pour chacune de ses fonctions:

  • CompletableFuture#<function>Async(..., Executor ex) Exécute cette fonction en passant par ex
  • CompletableFuture#<function>Async(...) Exécute cette fonction en passant par ForkJoinPool.commonPool()
  • CompletableFuture#<function>(...) Exécute cette fonction sur un des thread du CompletableFuture précédemment complété.
import java.util.concurrent.CompletableFuture;

SpongeExecutorService minecraftExecutor = Sponge.getScheduler().createSyncExecutor(plugin);

CompletableFuture.supplyAsync(() -> {
    // ASYNC: ForkJoinPool.commonPool()
    return 42;
}).thenAcceptAsync((awesomeValue) -> {
    // SYNC: minecraftExecutor
}, minecraftExecutor).thenRun(() -> {
    // SYNC: minecraftExecutor
});

RxJava

RxJava est une implémentation du concept de Reactive Extensions pour la JVM.

Le multithreading avec Rx est géré par plusieurs Schedulers. En utilisant la fonction Schedulers#from(Executor executor), l”Executor fournit par Sponge peut être transformé en Scheduler.

Un peu comme CompletableFuture, par défaut les actions sont exécutées sur le même thread qui a complété la partie précédente de la chaîne. Utilisez Observable#observeOn(Scheduler scheduler) pour changer de thread.

Une chose importante à garder à l’esprit est que l”Observable racine est invoqué sur le thread sur lequel Observable#subscribe() a été appelé. Si l’observable racine interagit avec Sponge, il doit être obligé d’être exécuté de manière synchrone en utilisant Observable#subscribeOn(Scheduler scheduler).

import rx.Observable;
import rx.Scheduler;
import rx.schedulers.Schedulers;

SpongeExecutorService executor = Sponge.getScheduler().createSyncExecutor(plugin);
Scheduler minecraftScheduler = Schedulers.from(executor);

Observable.defer(() -> Observable.from(Sponge.getServer().getOnlinePlayers()))
          .subscribeOn(minecraftScheduler) // defer -> SYNC: minecraftScheduler
          .observeOn(Schedulers.io()) // -> ASYNC: Schedulers.io()
          .filter(player -> {
              // ASYNC: Schedulers.io()
              return "Flards".equals(player.getName());
          })
          .observeOn(minecraftScheduler) // -> SYNC: minecraftScheduler
          .subscribe(player -> {
              // SYNC: minecraftScheduler
              player.kick(Text.of("Computer says no"));
          });

Scala

Scala vient avec un objet Future intégré que beaucoup de frameworks scala reflètent dans la conception. La plupart des méthodes du Future acceptent un ExecutionContext qui détermine où cette partie de l’opération doit être exécutée. C’est différent du CompletableFuture ou de RxJava puisque, eux, s’exécutent sur le même thread que celui où l’opération précédente s’est terminée.

Le fait que toutes ces opérations tentent implicitement de trouver un ExecutionContext signifie que vous pouvez facilement utiliser le ExecutionContext.global par défaut et exécuter plus précisément les parties qui doivent être thread-safe sur le thread du serveur Sponge.

Pour éviter d’accidentellement planifier du travail à travers l”ExecutorContext de Sponge, un autre contexte doit être implicitement défini pour qu’il devienne le choix par défaut. Pour maintenir la sécurité des threads seulement sur les fonctions qui interagissent avec Sponge vous devrez avoir l’exécuteur de Sponge spécifié.

import scala.concurrent.ExecutionContext

val executor = Sponge.getScheduler().createSyncExecutor(plugin)

import ExecutionContext.Implicits.global
val ec = ExecutionContext.fromExecutorService(executor)

val future = Future {
    // ASYNC: ExecutionContext.Implicits.global
}

future foreach {
    case value => // SYNC: ec
}(ec)

future map {
    case value => 42 // SYNC: ec
}(ec).foreach {
    case value => println(value) // ASYNC: ExecutionContext.Implicits.global
}