Планировщик

Предупреждение

Эти документы были написаны для 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)

delay(long delay,

TimeUnit unit)

Количество времени, прежде чем задача будет выполнена.

Время указывается в количестве тактов методом delayTicks(), а также время может быть передано более удобной единицей времени путем задания параметра TimeUnit в методе delay().

Можно указать любой метод, но не оба одновременно, для каждой задачи.

интервал

intervalTicks(

long interval)

interval(long interval,

TimeUnit unit)

Количество времени между повторами задачи. Если интервал не указан, то задача не будет повторяться.

Время указывается в количестве тактов методом intervalTicks(), а также время может быть передано более удобной единицей времени путем задания параметра TimeUnit в методе interval().

Можно указать любой метод, но не оба одновременно, для каждой задачи.

синхронизация

async()

Синхронная задача запускается в главном цикле игры. Если используется Task.Builder#async, задача будет выполняться асинхронно. Поэтому он будет работать в своем потоке, независимо от главного цикла, и не может безопасно использовать состояние игры. (См. Asynchronous Tasks.)

название

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, используйте следующие методы:

Следует помнить, что любые задачи, которые взаимодействуют с 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
}