Хостинг серверов Minecraft playvds.com
  1. Вы находитесь в русском сообществе Bukkit. Мы - администраторы серверов Minecraft, разрабатываем собственные плагины и переводим на русский язык плагины наших собратьев из других стран.
    Скрыть объявление

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

Тема в разделе "Разработка плагинов для новичков", создана пользователем Reality_SC, 12 фев 2015.

Статус темы:
Закрыта.
  1. Автор темы
    Reality_SC

    Reality_SC Старожил Пользователь

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

    Вдруг одолели сомнения по поводу алгоритма: параллельный поток (Thread) хочет сделать синхронную работу (обращаться к Bukkit API) в основном серверном цикле, после чего продолжить своё выполнение, используя результаты синхронного выполнения. Привожу тело метода public void run() этого потока:
    Код:
    /* 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
     
    Последнее редактирование: 8 апр 2016
  2. Хостинг MineCraft
    <
  3. HoShiMin

    HoShiMin Старожил Пользователь

    Баллы:
    173
    Я-то здесь при чём?) Я не джавист)
     
  4. Автор темы
    Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    А, ну ОКъ, пардонь :) Всё равно ты с толком :)
     
  5. serega6531

    serega6531 Старожил Девелопер Пользователь

    Баллы:
    173
    Skype:
    shkurovs
    Вызывай шелудером из потока синхронный runnable с нужным кодом.
     
  6. Автор темы
    Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    Я типа так и делаю, вопрос в том, корректно ли у меня в этом потоке ожидание завершения синхронной задачи.
     
  7. ptnk

    ptnk Старожил Пользователь

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

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

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

    В том случае, что ты написал выше по идее никаких wait'ов не нужно и никаких синхронный штучек, это просто спрятанный вызов обычного метода. Это то, как я понимаю данную ситуацию.
     
  8. Автор темы
    Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    То, что написано выше, является телом параллельного к основному потока, создание которого опущено для простоты.
    Пример: потенциально длительное обращение к , небольшая работа с апбдерез шедапишедулер ожиданием окончания его работы, затем опять длительная операция. Понятно, что модно разбить на два потока, но так короче и кошернее, казалось бы.[DOUBLEPOST=1423760142,1423759973][/DOUBLEPOST]Сраный ввод на телефоне :/
     
  9. ptnk

    ptnk Старожил Пользователь

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

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

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

    @Shevchik

    И ещё раз повторяю: в случае работы с нормальным параллелизмом не должно быть никакой работы c Bukkit Api.
     
  10. Автор темы
    Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    runTask блокируется на время выполнения Runnable на ближайшем серверном тике, а scheduleTask регистрирует его на выполнение и немедленно возвращает? Тогда это решает поставленный вопрос, да.[DOUBLEPOST=1423764051,1423763358][/DOUBLEPOST]Опять же, в самом баккит апи все написано не так:

    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 планируется на ближайший тик, а текущий поток побежал дальше.

    Добавлено.

    Итак, наступило мудрое утро, проводим тестирование.
    Я открыл чистый каркас шаблона плагина и написал такой класс:
    Код:
    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 в очередь исполнения и возвращают управление.

    Далее мне интересен вариант, предложенный мной в первом пост темы. Он по факту работает в моём плагине, но даже если не работает — я этого мог раньше не замечать. Поэтому тестирование в лабораторных условиях считаю уместным. Дописываем тест:
    Код:
    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 хранятся в различных полях и, соответственно, имеют предопределённую очерёдность выполнения.[DOUBLEPOST=1423802264][/DOUBLEPOST]P.S. Несколько различных вариаций запуска подтвердили, что:
    • Выглядит очень похоже на то, что все запланированные на определённый тик Runnable хранятся в HashSet и извлекаются из него беспорядочно.
    • У Runnable достаточно пометить метод public syncronized void run() вместо употребления в его теле блока syncronized(this) {}. Собственно, этот вариант и был придуман исходно, все тесты подтверждают его работоспособность.
    Считаю тему закрытой, спасибо всем, кто хотя бы прочитает её в будущем.
     
    Последнее редактирование: 19 июл 2015
  11. Shevchik

    Shevchik Старожил Пользователь

    Баллы:
    173
    Имя в Minecraft:
    _Shevchik_
    runTask шедулит исполнение и сразу же возвращает управление потоку вызвавшему ему. Для трюков типа ждать окончания выполнения из другого потока надо писать свои костыли либо callSyncTask(...).get().

    И все таски хранятся в одной очереди, односвязный список с хвостом в виде AtomicReference.
     
  12. Автор темы
    Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    callSyncMethod(Plugin plugin, Callable<T> task) — Calls a method on the main thread and returns a Future object.
    Ну да, тоже вариант, по-своему лаконичный. Спасибо.
     
  13. ptnk

    ptnk Старожил Пользователь

    Баллы:
    173
    Синхронизировать большие методы таким образом неэффективно.

    То, что ты там написал в спойлере - это гавно.
    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);
     
    Последнее редактирование: 13 фев 2015
  14. ql_Nik_lp

    ql_Nik_lp Старожил Девелопер Пользователь

    Баллы:
    173
    Skype:
    q-nik-p
    Имя в Minecraft:
    ql_Nik_lp
    Могу ли узнать зачем все эти манипуляции?
     
  15. Shevchik

    Shevchik Старожил Пользователь

    Баллы:
    173
    Имя в Minecraft:
    _Shevchik_
    Скорее всего что-то типа очереди где поток выполняет тяжёлые вычисления и с некоторым интервалом/количеством тасков выполняет что-то что не может выполняться асинхронно.[DOUBLEPOST=1423822016,1423821914][/DOUBLEPOST]P.S. Код тса хоть и пашет, но может обломаться если notify произойдёт раньше чем wait, а такое вполне может быть.
     
  16. Автор темы
    Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    Ну, конечно корректнее ставить synchronized(this) { notify(); } в самом конце тела Runnable, но если учесть во внимание, что ждёт его только один поток, и ждёт именно самого конца выполнения, то разница в подходах очень размывается.
    Согласен, теоретически стоит поставить булеву переменную и wait закинуть в цикл: while(!ready) wait();
    Представляешь, 5 секунд достаточно много, чтобы увидеть в логе сервера такую разницу по времени, за которую гарантированно произойдёт множество переключений контекста. Говно — потому что не работает так, как я заложил в него логику, или по любой другой причине?
    К сожалению, я не могу посоветовать тебе какую-либо книгу. Если такая существует, она должна называться "Как научиться понимать других людей". Я нисколько не сомневаюсь, что ты, возможно, намного дольше меня программируешь на Java, и знаешь больше хороших практик, чем я, но, пожалуйста, попробуй сначала прочитать и понять вопрос, попробовать на него ответить, а не искать в нём подложку из отборной бредятины.
    Я сразу написал в первом посту, что это параллельный поток, я прекрасно понимаю где код какого потока, и я никогда не собирался фризить сервер на 5 секунд.
    Я уверен, что делаю на 90-99% правильные вещи. Я прошу тебя опровергнуть свои же слова, если ты на самом деле прочитал код этого небольшого теста и вник в его логику, а не пишешь мне что я ничего не знаю "на отъебись".

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

    Вложения:

    Последнее редактирование: 13 фев 2015
  17. ptnk

    ptnk Старожил Пользователь

    Баллы:
    173
    Вот опять на лицо не понимание многопоточного программирования.

    Извиняюсь, мне показалось, что не то заснуло. Я был не прав.

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

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

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

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

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

    Поэтому и нужно предварительно озадачиться вопросом многопоточности, изучить стратегии и паттерны.
     
    Последнее редактирование: 13 фев 2015
  18. Автор темы
    Reality_SC

    Reality_SC Старожил Пользователь

    Баллы:
    123
    Имя в Minecraft:
    Reality_SC
    У меня нет ни одного вызова API вне потока главного цикла сервера, кроме вызовов методов шедулера (ну и логгера, сделаем на него скидку для простоты :)).
    Они синхронны с тем потоком, который их вызвал, а с bukkit — если их вызвали в главном потоке. Хоть ты, хоть шедулер. Простое забивание процессорного времени и стека мусорными прологом и эпилогом метода.
    Доступно Спиготовское зеркало:
    Само существование шедулера как раз таки оправдано, только если он thread-safety. Не думаю, что в своё время грамотные программисты придумали его для планирования только из главного потока.
    BukkitWorker — вообще не пользуюсь, просто обёртка над Thread, как и BukkitRunnable над Runnable. Достаточно только самого шедулера.

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

    Тема закрыта.
     
    Последнее редактирование: 1 апр 2015
Статус темы:
Закрыта.

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