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)
|
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 Chacune de ces méthodes, mais pas les deux, peuvent être spécifiées par tâche. |
interval |
|
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 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 |
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)
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 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 parex
CompletableFuture#<function>Async(...)
Exécute cette fonction en passant parForkJoinPool.commonPool()
CompletableFuture#<function>(...)
Exécute cette fonction sur un des thread duCompletableFuture
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
}