Планировщик
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;
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)
Предупреждение
Доступ к игровым объектам вне основного потока может привести к сбоям, несоответствиям и другим проблемам, которых следует избегать. Если у вас есть длинная операция в другом потоке, используйте: doc:планировщик <../scheduler>, чтобы внести изменения в такие игровые объекты в основном потоке. Если вы хотите использовать игровой объект в другом потоке, используйте моментальный снимок экземпляра или отдельного контейнера данных.
Совместимость с другими библиотеками
По мере увеличения размера и масштаба вашего плагина вы можете начать использовать одну из многочисленных библиотек параллелизма, доступных для 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;
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
}