Планировщик
Предупреждение
Эти документы были написаны для SpongeAPI 7 и, вероятно, устаревшие. Если вы чувствуете, что вы можете помочь обновить их, пожалуйста, отправьте PR!
Sponge предоставляет Scheduler, позволяющий назначать задачи, которые выполнятся в будущем. Scheduler
предоставляет Task.Builder, в котором можно указать параметры задачи, такие как задержка, интервал, название, (а)синхронность и Runnable
(см. Параметры задач).
Создатель задач
Сначала получите экземпляр Task.Builder
:
import org.spongepowered.api.scheduler.Task;
Task.Builder taskBuilder = Task.builder();
Единственным обязательным параметром является Runnable, который можно указать с помощью Task.Builder#execute(Runnable):
taskBuilder.execute(new Runnable() {
public void run() {
logger.info("Yay! Schedulers!");
}
});
или с помощью синтаксиса Java 8: Task.Builder#execute(Runnable runnable)
taskBuilder.execute(
() -> {
logger.info("Yay! Schedulers!");
}
);
или с помощью синтаксиса Java 8: Task.Builder#execute(Consumer<Task> task)
taskBuilder.execute(
task -> {
logger.info("Yay! Schedulers! :" + task.getName());
}
);
Параметры задач
С помощью Task.Builder
можно указать другие, дополнительные параметры, описанные ниже.
Параметр |
Используемый метод |
Описание |
---|---|---|
задержка |
delayTicks(long delay)
|
Количество времени, прежде чем задача будет выполнена. Время указывается в количестве тактов методом Можно указать любой метод, но не оба одновременно, для каждой задачи. |
интервал |
|
Количество времени между повторами задачи. Если интервал не указан, то задача не будет повторяться. Время указывается в количестве тактов методом Можно указать любой метод, но не оба одновременно, для каждой задачи. |
синхронизация |
async() |
Синхронная задача запускается в главном цикле игры. Если используется |
название |
name(String name) |
Название задачи. По умолчанию, название задачи будет PLUGIN_ID «-» ( «A-» | «S-» ) SERIAL_ID. Например, имя задачи по умолчанию может выглядеть как «fooplugin-A-12». Нет двух активных задач, которые будут иметь одинаковый идентификатор для одного и того же типа синхронизации. Если указано имя задачи, оно должно быть информативным и помогать пользователям в отладке плагина. |
Наконец, отправьте задание планировщику, используя Task.Builder#submit(Object).
Вот и всё! Таким образом, полностью функциональная запланированная задача, которая будет выполняться асинхронно каждые 5 минут после начальной задержки в 100 миллисекунд, может быть построена и отправлена с использованием следующего кода:
import java.util.concurrent.TimeUnit;
PluginContainer plugin = ...;
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);
Чтобы отменить задачу, просто вызовите метод Task#cancel():
task.cancel();
If you need to cancel the task from within the runnable itself, you can instead opt to use a Consumer<Task>
in
order to access the task. The below example will schedule a task that will count down from 60 and cancel itself upon
reaching 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(Component.text("Remaining Time: "+seconds+"s"));
if (seconds < 1) {
task.cancel();
}
}
}
Предупреждение
Any scheduled tasks should either watch the current state of the Game or should be unregistered if they are no longer needed (e.g. during a GameStoppingServerEvent). This is of particular importance for the client because it can start and stop the server multiple times.
Асинхронные задачи
Асинхронные задачи должны использоваться в основном для кода, который может занять значительный промежуток времени, а именно запросы на другой сервер или базу данных. Если это делается в основном потоке, запрос на другой сервер может сильно повлиять на производительность игры, так как следующий тик не может быть запущен до тех пор, пока запрос не будет завершен.
Since Minecraft is largely single-threaded, there is little you can do in an asynchronous thread. If you must run a thread asynchronously, you should execute all of the code that does not use SpongeAPI/affect Minecraft, then register another synchronous task to handle the code that needs the API. There are a few parts of Minecraft that you can work with asynchronously, including:
Чат
Встроенная обработка разрешений Sponge
Планировщик от Sponge
Кроме того, есть несколько других операций, которые безопасно выполнять асинхронно:
Независимые сетевые запросы
Файловая система ввода/вывода (исключая файлы, используемые Sponge)
Предупреждение
Доступ к игровым объектам вне основного потока может привести к сбоям, несоответствиям и другим проблемам, которых следует избегать. Если у вас есть длинная операция в другом потоке, используйте: doc:планировщик <../scheduler>, чтобы внести изменения в такие игровые объекты в основном потоке. Если вы хотите использовать игровой объект в другом потоке, используйте моментальный снимок экземпляра или отдельного контейнера данных.
Предупреждение
Any scheduled tasks should either watch the current state of the Game or should be unregistered if they are no longer needed (e.g. during a GameStoppingServerEvent). This is of particular importance for the client because it can start and stop the server multiple times.
Совместимость с другими библиотеками
По мере увеличения размера и масштаба вашего плагина вы можете начать использовать одну из многочисленных библиотек параллелизма, доступных для Java и JVM. Эти библиотеки, как правило, поддерживают ExecutorService как средство управления потоком выполнения задачи.
Чтобы эти библиотеки работали со Sponge Scheduler
, используйте следующие методы:
Scheduler#createSyncExecutor(Object) создает SpongeExecutorService, который выполняет задачи посредством синхронизации планировщика от Sponge.
Scheduler#createAsyncExecutor(Object) создает
SpongeExecutorService
, который выполняет задачи посредством асинхронного планировщика от Sponge. Задачи имеют ограничения упомянутые в разделе Asynchronous Tasks.
Следует помнить, что любые задачи, которые взаимодействуют с Sponge вне взаимодействий, перечисленных в Asynchronous Tasks, должны выполняться на ExecutorService, созданном с помощью Scheduler#createSyncExecutor(Object)
, чтобы быть потокобезопасными.
import org.spongepowered.api.scheduler.SpongeExecutorService;
PluginContainer plugin = ...;
SpongeExecutorService minecraftExecutor = Sponge.getScheduler().createSyncExecutor(plugin);
minecraftExecutor.submit(() -> { ... });
minecraftExecutor.schedule(() -> { ... }, 10, TimeUnit.SECONDS);
Почти во всех библиотеках есть какой-то способ адаптации ExecutorService
к планировщику задач. В качестве примера следующие параграфы объяснят, как ExecutorService
используется в ряде библиотек.
CompletableFuture (Java 8)
С Java 8 объект CompletableFuture был добавлен в стандартную библиотеку. По сравнению с объектом Future
, это позволяет разработчику предоставлять обратный вызов, который вызывается, когда будущее завершится, а не блокирует поток до тех пор, пока будущее не завершится.
CompletedFuture - это интерфейс, который обычно имеет следующие три варианта для каждой из своих функций:
CompletableFuture#<function>Async(..., Executor ex)
Выполняет эту функцию черезex
CompletableFuture#<function>Async(...)
Выполняет эту функцию черезForkJoinPool.commonPool()
CompletableFuture#<function>(...)
Выполняет эту функцию в любом потоке, на котором был завершен предыдущийCompletableFuture
.
import java.util.concurrent.CompletableFuture;
PluginContainer plugin = ...;
SpongeExecutorService minecraftExecutor = Sponge.getScheduler().createSyncExecutor(plugin);
CompletableFuture.supplyAsync(() -> {
// ASYNC: ForkJoinPool.commonPool()
return 42;
}).thenAcceptAsync((awesomeValue) -> {
// SYNC: minecraftExecutor
}, minecraftExecutor).thenRun(() -> {
// SYNC: minecraftExecutor
});
RxJava
RxJava - это реализация концепции Reactive Extensions для JVM.
Многопоточность в Rx управляется с помощью различных Schedulers. Используя функцию Schedulers#from(Executor executor)
, параметр Executor
, предоставленный Sponge, может быть преобразован в Scheduler
.
Подобно CompletableFuture
по умолчанию, действия выполняются в том же потоке, который завершил предыдущую часть цепочки. Используйте Observable#observeOn(Scheduler scheduler)
для перемещения между потоками.
One important thing to bear in mind is that the root Observable
gets invoked on whatever thread
Observable#subscribe()
was called on. If the root observable interacts with Sponge it should be forced to run
synchronously using Observable#subscribeOn(Scheduler scheduler)
.
import rx.Observable;
import rx.Scheduler;
import rx.schedulers.Schedulers;
PluginContainer plugin = ...;
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(Component.text("Computer says no"));
});
Scala
Scala comes with a built-in Future object which a lot of scala framework mirror in design. Most methods of the Future accept an ExecutionContext which determined where that part of the operation is executed. This is different from the CompletableFuture or RxJava since they default to executing on the same thread on which the previous operation ended.
The fact that all these operations try to implicitly find an ExecutionContext
means that you can easily use
the default ExecutionContext.global
and specifically run the parts that need to be thread-safe on the Sponge
server thread.
To avoid accidentally scheduling work on through the Sponge ExecutorContext
another context should be implicitly
defined so it acts as the default choice. To maintain thread safety only the functions that actually interact with Sponge
will need to have the Sponge executor specified.
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
}