Войти или зарегистрироваться
  • Вы находитесь в сообществе Rubukkit. Мы - администраторы серверов Minecraft, разрабатываем собственные плагины и переводим на различные языки плагины наших коллег из других стран.

[Решено] Корректна ли синхронизация? (async -> sync -> async)

Статус
Закрыто для дальнейших ответов.

Reality_SC

Старожил
Пользователь
Баллы
123
Имя в Minecraft
Reality_SC
UPDATE: Тесты (ниже) показали, что это действительно хороший рабочий вариант. Можно пользоваться.

Вдруг одолели сомнения по поводу алгоритма: параллельный поток (Thread) хочет сделать синхронную работу (обращаться к Bukkit API) в основном серверном цикле, после чего продолжить своё выполнение, используя результаты синхронного выполнения. Привожу тело метода public void run() этого потока:
Java:
/* SOMEWHERE IN THE MAIN SERVER THREAD ... */
...
final Thread thread = new Thread()
{
   @Override
   public void run()
   {
      ...
      /* Do first parallel part HERE */
      ...
      final Runnable syncTask = new Runnable()
      {
         @Override
         public synchronized void run()
         {
            ...
            /* Do some sync work with API HERE */
            ...
            notify();
         }
      };
      try
      {
         synchronized(syncTask)
         {
            plugin.getServer().getScheduler().runTask(plugin, syncTask);
            syncTask.wait();
         }
      } catch(InterruptedException ex) {
      }
      ...
      /* Do last parallel part HERE */
      ...
   }
};
// Start execution
thread.start();
...
/* CONTINUE WITH MAIN THREAD */

Корректно ли это написано? Фактически никогда ранее не пользовался wait/notify, мне ближе мьютексы и критические секции =\

@ptnk, @fromgate, @alexandrage, @HoShiMin, etc
 
Последнее редактирование:
Вызывай шелудером из потока синхронный runnable с нужным кодом.
 
Вызывай шелудером из потока синхронный runnable с нужным кодом.
plugin.getServer().getScheduler().runTask(plugin, syncTask);
Я типа так и делаю, вопрос в том, корректно ли у меня в этом потоке ожидание завершения синхронной задачи.
 
Вдруг одолели сомнения по поводу алгоритма: параллельный поток хочет сделать синхронную работу в основном серверном цикле и продолжить своё выполнение:
Java:
...
/* DO FIRST PARALLEL PART HERE */
...
final Runnable syncTask = new Runnable()
{
    @Override
    public synchronized void run()
    {
        ...
        /* DO SOME SYNC WORK WITH API HERE */
        ...
        notify();
    }
};
try
{
    synchronized(syncTask)
    {
        plugin.getServer().getScheduler().runTask(plugin, syncTask);
        syncTask.wait();
    }
} catch(InterruptedException ex) {
}
...
/* DO LAST PARALLEL PART HERE */
...

Корректно ли это написано? Редко пользуюсь wait/notify, мне ближе мьютексы и критические секции =\

@ptnk, @fromgate, @alexandrage, @HoShiMin, etc

По исходникам видно, что вызывается обычная задача, а я что-то не припоминаю, чтобы обычные задачи были многопоточными.

Т.е. bukkit работает в один поток и вызывает эти задачки друг за дружкой, не требуя синхронизации.
Другое дело Async-Task - это уже полноценная обёртка над Thread t = new Thred(){}; t.run().

Короче, Async Task - это Обычный Thread.run() и они требуют синхронизации и в них нельзя использовать Bukkit Api.

В том случае, что ты написал выше по идее никаких wait'ов не нужно и никаких синхронный штучек, это просто спрятанный вызов обычного метода. Это то, как я понимаю данную ситуацию.
 
В том случае, что ты написал выше по идее никаких wait'ов не нужно и никаких синхронный штучек, это просто спрятанный вызов обычного метода. Это то, как я понимаю данную ситуацию.
То, что написано выше, является телом параллельного к основному потока, создание которого опущено для простоты.
Пример: потенциально длительное обращение к , небольшая работа с апбдерез шедапишедулер ожиданием окончания его работы, затем опять длительная операция. Понятно, что модно разбить на два потока, но так короче и кошернее, казалось бы.
Сраный ввод на телефоне :/
 
То, что написано выше, является телом параллельного к основному потока, создание которого опущено для простоты.
Пример: потенциально длительное обращение к , небольшая работа с апбдерез шедапишедулер ожиданием окончания его работы, затем опять длительная операция. Понятно, что модно разбить на два потока, но так короче и кошернее, казалось бы.
Сраный ввод на телефоне :/

plugin.getServer().getScheduler().runTask(plugin, syncTask);
Это не параллельный поток.

1) plugin.getServer().getScheduler().runTask(plugin, syncTask);
2) Logger.infog("after");

Строчка 1 запускает задачку, и пока задачка не завершится - логгер не выведет "after".
Поэтому здесь думать не нужно.

@Shevchik

И ещё раз повторяю: в случае работы с нормальным параллелизмом не должно быть никакой работы c Bukkit Api.
 
runTask блокируется на время выполнения Runnable на ближайшем серверном тике, а scheduleTask регистрирует его на выполнение и немедленно возвращает? Тогда это решает поставленный вопрос, да.
Опять же, в самом баккит апи все написано не так:

runTask(Plugin plugin, Runnable task)
Returns a task that will run on the next server tick.

scheduleSyncDelayedTask(Plugin plugin, Runnable task)
Schedules a once off task to occur as soon as possible.

Такое чувство, что результат их вызова одинаков — Runnable планируется на ближайший тик, а текущий поток побежал дальше.

Добавлено.

Итак, наступило мудрое утро, проводим тестирование.
Я открыл чистый каркас шаблона плагина и написал такой класс:
Java:
package org.rubukkit.template;

public class SyncTest
{
    private final BukkitPluginMain plugin;
    public SyncTest(final BukkitPluginMain plugin)
    {
        this.plugin = plugin;
    }
    public void runTest()
    {
        BukkitPluginMain.consoleLog.info("STARTING PARALLEL THREAD");
        new Thread()
        {
            @Override
            public void run()
            {
                BukkitPluginMain.consoleLog.info("PARALLEL THREAD HAS BEEN STARTED");
                final Runnable r1 = new Runnable()
                {
                    @Override
                    public void run()
                    {
                        BukkitPluginMain.consoleLog.info("SOME SYNC WORK");
                        try
                        {
                            Thread.sleep(5000); // 5 sec
                        } catch(InterruptedException ex) {
                        }
                    }
                };
                plugin.getServer().getScheduler().runTask(plugin, r1);
                BukkitPluginMain.consoleLog.info("PARALLEL THREAD CONTINUES AFTER runTask");
                plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, r1);
                BukkitPluginMain.consoleLog.info("PARALLEL THREAD CONTINUES AFTER scheduleSyncDelayedTask");
                BukkitPluginMain.consoleLog.info("PARALLEL THREAD HAS BEEN FINISHED");
            }
        }.start();
    }
}
В onEnable() плагина запускаем тест: new SyncTask(this).runTest();
Проводим тестирование на чистом сервере, в папке plugins лежит только этот плагин. Лог запуска:
Код:
[11:01:35] [Server thread/INFO]: Starting minecraft server version 1.8
[11:01:37] [Server thread/INFO]: [template] Enabling template v0.2.8
[11:01:37] [Server thread/INFO]: STARTING PARALLEL THREAD
[11:01:37] [Thread-8/INFO]: PARALLEL THREAD HAS BEEN STARTED
[11:01:37] [Thread-8/INFO]: PARALLEL THREAD CONTINUES AFTER runTask
[11:01:37] [Thread-8/INFO]: PARALLEL THREAD CONTINUES AFTER scheduleSyncDelayedTask
[11:01:37] [Thread-8/INFO]: PARALLEL THREAD HAS BEEN FINISHED
[11:01:37] [Server thread/INFO]: [RBTemplate] Plugin has been successfully enabled.
[11:01:37] [Server thread/INFO]: Done (1,209s)! For help, type "help" or "?"
[11:01:37] [Server thread/INFO]: SOME SYNC WORK
[11:01:42] [Server thread/INFO]: SOME SYNC WORK
Как можно заметить, совет, данный @ptnk выше, не работает. И runTask, и scheduleSyncDelayedTask выполняют одинаковую работу — немедленно помещают указанный Runnable в очередь исполнения и возвращают управление.

Далее мне интересен вариант, предложенный мной в первом пост темы. Он по факту работает в моём плагине, но даже если не работает — я этого мог раньше не замечать. Поэтому тестирование в лабораторных условиях считаю уместным. Дописываем тест:
Java:
public void runTest()
{
    BukkitPluginMain.consoleLog.info("STARTING PARALLEL THREAD");
    new Thread()
    {
        @Override
        public void run()
        {
            BukkitPluginMain.consoleLog.info("PARALLEL THREAD HAS BEEN STARTED");
            final Runnable r1 = new Runnable()
            {
                @Override
                public void run()
                {
                    BukkitPluginMain.consoleLog.info("SOME SYNC WORK (r1)");
                    try
                    {
                        Thread.sleep(5000); // 5 sec
                    } catch(InterruptedException ex) {
                    }
                }
            };
            final Runnable r2 = new Runnable()
            {
                @Override
                public void run()
                {
                    BukkitPluginMain.consoleLog.info("SOME SYNC WORK (r2)");
                    try
                    {
                        Thread.sleep(5000); // 5 sec
                    } catch(InterruptedException ex) {
                    }
                    synchronized(this)
                    {
                        notify();
                    }
                }
            };
            plugin.getServer().getScheduler().runTask(plugin, r1);
            BukkitPluginMain.consoleLog.info("PARALLEL THREAD CONTINUES AFTER runTask");
            plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, r1);
            BukkitPluginMain.consoleLog.info("PARALLEL THREAD CONTINUES AFTER scheduleSyncDelayedTask");
            try
            {
                synchronized(r2)
                {
                    plugin.getServer().getScheduler().runTask(plugin, r2);
                    r2.wait();
                }
            } catch(InterruptedException ex) {
            }
            BukkitPluginMain.consoleLog.info("PARALLEL THREAD CONTINUES AFTER syncronized runTask");
            BukkitPluginMain.consoleLog.info("PARALLEL THREAD HAS BEEN FINISHED");
        }
    }.start();
}
Лог запуска:
Код:
[11:15:51] [Server thread/INFO]: Starting minecraft server version 1.8
[11:15:53] [Server thread/INFO]: [template] Enabling template v0.2.8
[11:15:53] [Server thread/INFO]: STARTING PARALLEL THREAD
[11:15:53] [Thread-7/INFO]: PARALLEL THREAD HAS BEEN STARTED
[11:15:53] [Thread-7/INFO]: PARALLEL THREAD CONTINUES AFTER runTask
[11:15:53] [Thread-7/INFO]: PARALLEL THREAD CONTINUES AFTER scheduleSyncDelayedTask
[11:15:53] [Server thread/INFO]: [RBTemplate] Plugin has been successfully enabled.
[11:15:53] [Server thread/INFO]: Done (1,160s)! For help, type "help" or "?"
[11:15:53] [Server thread/INFO]: SOME SYNC WORK (r1)
[11:15:58] [Server thread/INFO]: SOME SYNC WORK (r2)
[11:16:03] [Server thread/INFO]: SOME SYNC WORK (r1)
[11:16:03] [Thread-7/INFO]: PARALLEL THREAD CONTINUES AFTER syncronized runTask
[11:16:03] [Thread-7/INFO]: PARALLEL THREAD HAS BEEN FINISHED
Как мы видим, параллельный поток дождался выполнения шедулером синхронного Runnable, и только затем продолжил своё выполнение. Дополнительный факт, что один из r1 запустился после него. Это говорит о том, что а) все r1 и r2 были запланированы на один тик, и б) либо они хранятся в Set и берутся оттуда в произвольном порядке, либо Runnable-ы переданные в runTask и в scheduleSyncDelayedTask хранятся в различных полях и, соответственно, имеют предопределённую очерёдность выполнения.
P.S. Несколько различных вариаций запуска подтвердили, что:
  • Выглядит очень похоже на то, что все запланированные на определённый тик Runnable хранятся в HashSet и извлекаются из него беспорядочно.
  • У Runnable достаточно пометить метод public syncronized void run() вместо употребления в его теле блока syncronized(this) {}. Собственно, этот вариант и был придуман исходно, все тесты подтверждают его работоспособность.
Считаю тему закрытой, спасибо всем, кто хотя бы прочитает её в будущем.
 
Последнее редактирование:
runTask шедулит исполнение и сразу же возвращает управление потоку вызвавшему ему. Для трюков типа ждать окончания выполнения из другого потока надо писать свои костыли либо callSyncTask(...).get().

И все таски хранятся в одной очереди, односвязный список с хвостом в виде AtomicReference.
 
  • У Runnable достаточно пометить метод public syncronized void run() вместо употребления в его теле блока syncronized(this) {}. Собственно, этот вариант и был придуман исходно, все тесты подтверждают его работоспособность..
Синхронизировать большие методы таким образом неэффективно.

То, что ты там написал в спойлере - это гавно.
Thread.sleep(5000);// 5 sec - это тормозит основной поток. Представляешь, если у тебя игроки зафризятся?

Книга называется Java Concurrency In Practice, это для тех, кто хочет получше понять, как работать с многопоточностью в java.

Runnable.run() и Thread.start() - это различные вещи, пока, ты не особо понимаешь очевидных вещей, похоже ты даже не знаешь основных проблем, которые возникают в многопоточном программировании и из-за этого ты делаешь неправильные вещи.


Здесь модет быть ошибка
  1. plugin.getServer().getScheduler().runTask(plugin, r1);
  2. plugin.getServer().getScheduler().scheduleSyncDelayedTask(plugin, r1);
 
Последнее редактирование:
Могу ли узнать зачем все эти манипуляции?
 
Скорее всего что-то типа очереди где поток выполняет тяжёлые вычисления и с некоторым интервалом/количеством тасков выполняет что-то что не может выполняться асинхронно.
P.S. Код тса хоть и пашет, но может обломаться если notify произойдёт раньше чем wait, а такое вполне может быть.
 
Синхронизировать большие методы таким образом неэффективно.
Ну, конечно корректнее ставить synchronized(this) { notify(); } в самом конце тела Runnable, но если учесть во внимание, что ждёт его только один поток, и ждёт именно самого конца выполнения, то разница в подходах очень размывается.
Код тса хоть и пашет, но может обломаться если notify произойдёт раньше чем wait, а такое вполне может быть.
Согласен, теоретически стоит поставить булеву переменную и wait закинуть в цикл: while(!ready) wait();
То, что ты там написал в спойлере — это гавно.
Thread.sleep(5000); // 5 sec - это тормозит основной поток. Представляешь, если у тебя игроки зафризятся?
Представляешь, 5 секунд достаточно много, чтобы увидеть в логе сервера такую разницу по времени, за которую гарантированно произойдёт множество переключений контекста. Говно — потому что не работает так, как я заложил в него логику, или по любой другой причине?
Книга называется Java Concurrency In Practice, это для тех, кто хочет получше понять, как работать с многопоточностью в java.
К сожалению, я не могу посоветовать тебе какую-либо книгу. Если такая существует, она должна называться "Как научиться понимать других людей". Я нисколько не сомневаюсь, что ты, возможно, намного дольше меня программируешь на Java, и знаешь больше хороших практик, чем я, но, пожалуйста, попробуй сначала прочитать и понять вопрос, попробовать на него ответить, а не искать в нём подложку из отборной бредятины.
Я сразу написал в первом посту, что это параллельный поток, я прекрасно понимаю где код какого потока, и я никогда не собирался фризить сервер на 5 секунд.
Runnable.run() и Thread.start() — это различные вещи, пока, ты не особо понимаешь очевидных вещей, похоже ты даже не знаешь основных проблем, которые возникают в многопоточном программировании и из-за этого ты делаешь неправильные вещи.
Я уверен, что делаю на 90-99% правильные вещи. Я прошу тебя опровергнуть свои же слова, если ты на самом деле прочитал код этого небольшого теста и вник в его логику, а не пишешь мне что я ничего не знаю "на отъебись".

Прикладываю полноценный компилируемый и запускаемый исходник + .jar.
Могу ли узнать зачем все эти манипуляции?
Задача родилась фактически на пустом месте. Она не является неразрешимой, и если бы я понятия не имел о том, что вообще значат ключевые слова wait/notify, я бы обошёл эту проблему за пять минут.
Относись к ней, как к учебной, не более.
 

Вложения:

Последнее редактирование:
Ну, конечно корректнее ставить synchronized(this) { notify(); } в самом конце тела Runnable
Вот опять на лицо не понимание многопоточного программирования.

Представляешь, 5 секунд достаточно много, чтобы увидеть в логе сервера такую разницу по времени, за которую гарантированно произойдёт множество переключений контекста. Говно — потому что не работает так, как я заложил в него логику, или по любой другой причине?
Извиняюсь, мне показалось, что не то заснуло. Я был не прав.

Я сразу написал в первом посту, что это параллельный поток, я прекрасно понимаю где код какого потока, и я никогда не собирался фризить сервер на 5 секунд.

Я тебе уже в четвёртый раз пишу, что никаких Bukkit-API во внешних потоках быть не должно, если нигде в документации не написано специально "thread-safety)".

Я тебе пишу про это, потому, что у меня нет уверенности, что CraftScheduler обладает таким свойством, потому, что оффициальная документация недоступна.

Для меня все синхронные таски в bukkit это Runnable.run(), т.е. вызов метода в том же самом потоке.
А вот все асинхронные таски в bukkit Thread.start(), т.к. метод крутиться в соседнем потоке [Нет нормальных исходников, чтобы проследить, где там и как запускаются BukkitWorker, но у меня ощущение, что именно Thread.start() для них делается, поэтому они и не Thread-Safety и поэтому они Deprecated и написано, чтобы не использовали в них Bukkit-Api].

На мой взгляд то, что у тебя в примере написано таит в себе скрытую угрозу т.к. CraftScheduler может быть не потоко безопасным, поэтому и Bukkit Api из других потоков использовать нельзя из-за возможности нарваться на эксепшены, а это значит, что нужно искать другие стратегии для решения твоей потребности.

Если у тебя всегда строго какой-то ресурс нужно разделить между двумя потоками, то почему бы просто не воспользоваться коллекцией из conurrency-пакета? Один поток на чтение и один поток на запись синхронизированной коллекции проблем не вызывут и никаких лишних телодвижений не будет.

Поэтому и нужно предварительно озадачиться вопросом многопоточности, изучить стратегии и паттерны.
 
Последнее редактирование:
Я тебе уже в четвёртый раз пишу, что никаких Bukkit-API во внешних потоках быть не должно, если нигде в документации не написано специально "thread-safety".
У меня нет ни одного вызова API вне потока главного цикла сервера, кроме вызовов методов шедулера (ну и логгера, сделаем на него скидку для простоты :)).
Для меня все синхронные таски в bukkit это Runnable.run(), т.е. вызов метода в том же самом потоке.
Они синхронны с тем потоком, который их вызвал, а с bukkit — если их вызвали в главном потоке. Хоть ты, хоть шедулер. Простое забивание процессорного времени и стека мусорными прологом и эпилогом метода.
Я тебе пишу про это, потому, что у меня нет уверенности, что CraftScheduler обладает таким свойством, потому, что официальная документация недоступна.
Доступно Спиготовское зеркало:
Package org.bukkit.scheduler
Classes dedicated to letting plugins run code at specific time intervals, including thread safety.
Само существование шедулера как раз таки оправдано, только если он thread-safety. Не думаю, что в своё время грамотные программисты придумали его для планирования только из главного потока.
BukkitWorker — вообще не пользуюсь, просто обёртка над Thread, как и BukkitRunnable над Runnable. Достаточно только самого шедулера.

Ладно, предлагаю закрыть дискуссию, и считать, что мы друг друга поняли.

Тема закрыта.
 
Последнее редактирование:
Статус
Закрыто для дальнейших ответов.

Поделиться этой страницей

Назад
Верх