Оригінальна стаття https://docs.mineland.net/docs/dev/async/
Дата 31.07.2021
В bukkit существует главный поток сервера Server thread
, в котором выполняется обработка всех блоков, мобов и плагинов. Этот поток выполняет 20 обработок в секунду, каждая такая обработка называется тиком. Легко посчитать, что один тик должен занимать не более 50ms. Если тик не будет успевать выполняться за 50ms, тогда сервер начнет лагать. В главном потоке сервера нельзя выполнять код, который будет блокировать этот поток, потому что блокировка занимает время.
Блокировка потока - это операции ввода/вывода, синхронизации, вызов метода Thread.sleep()
. Ярким примером блокирующей операции является запрос в базу данных или redis.
Также в bukkit существуют netty потоки, в которых выполняется обработка всех пакетов между bukkit-сервером и игроком или bungeecord-сервером. Эти пакеты должны обратываться максимально быстро с момента их отправки или приема. Задержка в обработке пакетов будет увеличивать пинг игроков. Netty потоки также не должны блокироваться.
Такие потоки, как главный поток сервера или netty потоки, в которых блокировка будет приводить к ухудшению производительности сервера, я буду называть нежелательными для блокировки потоками
.
Практически все, с чем приходится иметь дело, работает в нежелательных для блокировки потоках, а именно:
- События
@EventHandler
в bukkit/bungeecord. - Все команды bukkit/bungeecord.
- ProtocolLib события пакетов в bukkit.
Любые блокирующие операции не должны выполняться в нежелательных для блокировки потоках. Такие операции лучше всего выносить в асинхронные потоки. В проекте для общего пользования создан общий пул асинхронных потоков, который хранится в api-классе net.mineland.core.api.AsyncScheduler
.
Выполнение запроса к базе данных в асинхронном потоке:
AsyncScheduler.run(() -> {
SQL.getJooq().execute("update players set email = '50ll@mail.ru' where name = ?", "Lokha");
});
Асинхронность и bukkit api
Но будьте осторожны, обращения к bukkit api из асинхронного потока может приводить к ошибкам. Во-первых, bukkit не потокобезопасный, во-вторых, в некоторых методах bukkit специально стоит проверка на текущий поток, которая кинет ошибку, если текущий поток не является главным потоком сервера. Это атуально для ситуаций, когда нужно обработать полученные данные из базы данных в главном потоке сервера. Чтобы вызвать код в главном потоке сервера, нужно использовать api-класс net.mineland.bukkit.api.Scheduler
.
Выполнение запроса к базе данных в асинхронном потоке, и обработка его результата в главном потоке сервера:
@EventHandler
public void on(PlayerJoinEvent event) {
AsyncScheduler.run(() -> { // вызываем скачивание из базы данных асинхронно
Record record = SQL.getJooq().fetchOne("select world, x, y, z from homes where name = ?", "Lokha");
Scheduler.run(() -> { // вызываем обработку record в ближайшем тике в главном потоке сервера
World world = Bukkit.getWorld(record.get("world", String.class));
int x = record.get("x", int.class);
int y = record.get("y", int.class);
int z = record.get("z", int.class);
event.getPlayer().teleport(new Location(world, x, y, z));
});
});
}
Класс Scheduler
является просто оболочкой для Bukkit.getScheduler()
. А AsyncScheduler
работает на основе обычного ExecutorService, которого можно достать методом AsyncScheduler.getExecutor()
.
Асинхронные события в bukkit
В bukkit имеются события, которые вызываются из асинхронных потоков и в них разрешается блокировка. Все такие события в именах их классов содержат слово Async
.
Пример sql запроса в асинхронном событии:
@EventHandler
public void on(AsyncPlayerPreLoginEvent event) {
SQL.getJooq().execute("update players set last_online = NOW() where name = ?", event.getName());
}
Событие AsyncPlayerPreLoginEvent
вызывается при входе игрока на сервер. Его можно блокировать, как показано в примере. Блокировка этого события задерживает вход игрока на сервер. Если sql запрос будет занимать 1 секунду, тогда процесс логина увеличится на 1 секунду, игрок будет видеть экран загрузки мира. По этому блокировку этого события нужно использовать в исключительных ситуациях. В данный момент он используется, чтобы загрузить аккаунт, скин и права игрока.
Асинхронные события в bungeecord
В bungeecord нет событий, которые бы вызывались из асинхронных потоков, но поддержка асинхроности имеется в некоторых событиях. Все такие события в наследуются от net.md_5.bungee.api.event.AsyncEvent
.
Класс AsyncEvent
добавляет наследникам методы void registerIndent(Plugin plugin)
и void completeIndent(Plugin plugin)
. Вызвав метод registerIndent
, мы просим событие отложить окончание своей работы до тех пор, пока мы не вызовем completeIndent
.
Пример sql запроса в асинхронном событии:
@EventHandler
public void on(LoginEvent event){
event.registerIntent(BungeeLibsPlugin.getInstance());
AsyncScheduler.run(() -> {
try{
SQL.getJooq().execute("update players set last_online = NOW() where name = ?", event.getName());
} finally{
event.completeIntent(BungeeLibsPlugin.getInstance());
}
});
}
Событие LoginEvent
вызывается при подключении игрока в банге. С помощью вызова registerIntent
мы просим событие отложить окончание своей работы. Потом мы вызываем sql запрос в асинхронном потоке. После выполнения запроса вызываем completeIntent
, чтобы событие закончило свою работу. Если случится такое, что метод completeIntent
не будет вызван, тогда у игрока будет вечный экран загрузки мира, по этому я поместил вызов этого метода в блок finally
. Задержка с вызовом completeIntent
будет задержкой логина игрока на сервер. По этому использование registerIntent
рекомендуется только в необходимых для этого ситуациях. В данный момент он используется, чтобы загрузить аккаунт, последний сервер, скин и права игрока.