Параллелизм высокого уровня в игровом цикле Android
Я пытаюсь синхронизировать пару потоков, не связанных с пользовательским интерфейсом, один поток для запуска игровой логики и один поток для рендеринга, чтобы выполнять задачи в логическом и эффективном порядке. Я наложил ограничение на то, что вся система работает в стационарном состоянии без выделения, поэтому все экранные объекты возвращаются и "перерабатываются", и поэтому два потока должны поддерживать своего рода двусторонний диалог, который возникает, когда я вызываю Метод swapBuffers().
В псевдокоде порядок событий в игровом потоке выглядит примерно так:
while(condition)
{
processUserInput();
runGameLogicCycle(set number of times);
waitForBuffersSwapped();
unloadRecycleBuffer(); //This function modifies one buffer
loadDisplayBuffer(); ///This function modifies another buffer
waitForRenderFinished();
renderThread.postTask(swapBuffersAndRender);
}
Поток рендеринга выбран для выполнения задачи замены буферов так, чтобы поток логики игры мог выполнять задачи, которые не изменяют буферы. В моем коде я объединяю задачу обмена буферов и рендеринга и определяю его как объект Runnable, который публикуется в обработчике потока рендеринга. В псевдокоде этот runnable выглядит примерно так:
{
//Swap the buffers
}
gameThread.notifyThatBuffersSwapped();
{
//Render items to screen
}
gameThread.notifyThatItemsRendered();
Моя проблема - проблема реализации. Я знаком с концепциями обработчиков, блоков синхронизации и ReentrantLocks. Мне известны методы Lock.await() Lock.signal(), но я нахожу документацию недостаточной, когда пытаюсь понять, как они ведут себя при вызове в цикле итерации.
Как реализовать ReentrantLocks, чтобы два потока ожидали друг друга таким образом? Пожалуйста, включите практическую идиому в ваш ответ, если это возможно.
1 ответ
Я не уверен, что замок это то, что вы хотите. Вы хотите, чтобы текущий поток имел монопольный доступ к объектам, но как только вы снимаете блокировку, вы хотите быть уверены, что другой поток выполнится до того, как вы снова получите блокировку.
Вы могли бы, например, использовать плохо названное ConditionVariable:
loop:
game thread: does stuff w/objects
game thread: cv1.close()
game thread: cv2.open()
[render thread now "owns" the objects]
game thread: does stuff w/o objects
game thread: cv1.block()
game thread: [blocks]
loop:
render thread: does stuff w/objects
render thread: cv2.close()
render thread: cv1.open()
[game thread now "owns" the objects]
render thread: cv2.block()
render thread: [blocks]
Это работает два потока в lockstep. Вы получаете некоторый параллелизм при выполнении операций с объектами, не являющимися общими, но это все.
java.util.concurrent
обеспечивает CountDownLatch
, но это объект с одним выстрелом, который идет вразрез с вашим желанием избежать выделения ресурсов. Фантазии CyclicBarrier
делает больше
Это не очень хорошее решение - хотя замки были не совсем то, что вы хотели, они были частью того, что вы хотели, и я покончил с ними здесь. Невозможно посмотреть на код и легко определить, что оба потока не могут работать с объектами одновременно.
Вы можете рассмотреть возможность двойной буферизации объектов. Вам потребовалось бы вдвое больше памяти, но проблемы синхронизации проще: каждый поток по существу имеет свой собственный набор данных для работы, и единственный раз, когда вам нужно сделать паузу, это когда игровой поток хочет поменять местами наборы. Параллельность максимальна. Если игровой поток опаздывает, поток рендеринга просто рисует то, что у него есть снова (если вы используете GLSurfaceView
) или пропускает рендеринг. (Вы можете использовать и тройной буфер для повышения пропускной способности, но это увеличивает использование памяти и задержку.)