Планировщик

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) The name of the task. By default, the name of the task will be PLUGIN_ID «-» ( «A-» | «S-» ) SERIAL_ID. For example, a default task name could look like «FooPlugin-A-12». No two active tasks will have the same serial ID for the same synchronization type. If a task name is specified, it should be descriptive and aid users in debugging your plugin.

Наконец, отправьте задание планировщику, используя Task.Builder#submit(Object).

Вот и всё! Таким образом, полностью функциональная запланированная задача, которая будет выполняться асинхронно каждые 5 минут после начальной задержки в 100 миллисекунд, может быть построена и отправлена с использованием следующего кода:

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);

Чтобы отменить задачу, просто вызовите метод Task#cancel():

task.cancel();

Если вам нужно отменить задачу именно из Runnable, вы можете вместо этого выбрать Consumer<Task>` для доступа к задаче. В следующем примере будет запланирована задача, которая будет отсчитывать от 60 и отменит себя при достижении 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();
        }
    }
}

Асинхронные задачи

Асинхронные задачи должны использоваться в основном для кода, который может занять значительный промежуток времени, а именно запросы на другой сервер или базу данных. Если это делается в основном потоке, запрос на другой сервер может сильно повлиять на производительность игры, так как следующий тик не может быть запущен до тех пор, пока запрос не будет завершен.

Поскольку Minecraft в основном однопоточный, в асинхронном потоке мало что можно сделать. Если вам необходимо запустить асинхронный поток, то код в асинхронном потоке не должен использовать SpongeAPI или взаимодействовать с Minecraft. Зарегистрируйте другую synchronous задачу для обработки кода, который требует SpongeAPI. Есть несколько частей Minecraft, с которыми вы можете работать asynchronously, в том числе:

  • Чат
  • Встроенная обработка разрешений Sponge
  • Планировщик от Sponge

Кроме того, есть несколько других операций, которые безопасно выполнять асинхронно:

  • Независимые сетевые запросы
  • Файловая система ввода/вывода (исключая файлы, используемые Sponge)

Совместимость с другими библиотеками

По мере увеличения размера и масштаба вашего плагина вы можете начать использовать одну из многочисленных библиотек параллелизма, доступных для Java и JVM. Эти библиотеки, как правило, поддерживают ExecutorService как средство управления потоком выполнения задачи.

Чтобы эти библиотеки работали со Sponge Scheduler, используйте следующие методы:

Следует помнить, что любые задачи, которые взаимодействуют с Sponge вне взаимодействий, перечисленных в Asynchronous Tasks, должны выполняться на ExecutorService, созданном с помощью Scheduler#createSyncExecutor(Object), чтобы быть потокобезопасными.

import org.spongepowered.api.scheduler.SpongeExecutorService;

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;

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;

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 поставляется со встроенным объектом Future, который повторяет большую часть Scala Framework. Большинство методов Future принимают ExecutionContext, который определяет, где выполняется эта часть операции. Это отличается от CompletableFuture или RxJava, поскольку они по умолчанию выполняются в том же потоке, в котором закончилась предыдущая операция.

Тот факт, что вся эта операция пытается неявно найти ExecutionContext, означает, что вы можете легко использовать стандартный ExecutionContext.global и специально запускать части, которые должны быть потокобезопасными для потока сервера Sponge.

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
}