Параллелизм высокого уровня в игровом цикле 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, который отправляется обработчику потока рендеринга. В псевдокоде этот исполняемый файл выглядит примерно так:

{
    //Swap the buffers
}
gameThread.notifyThatBuffersSwapped();
{
    //Render items to screen
}
gameThread.notifyThatItemsRendered();

Моя проблема связана с реализацией. Я знаком с концепциями обработчиков, блоков синхронизации и ReentrantLocks. Я знаю о методах Lock.await() Lock.signal(), но мне кажется, что документации недостаточно, чтобы понять, как они ведут себя при вызове в цикле итераций.

Как реализовать ReentrantLocks, чтобы заставить два потока ожидать друг друга таким образом? Пожалуйста, включите практическую идиому в свой ответ, если это возможно.


person Boston Walker    schedule 04.06.2013    source источник


Ответы (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]

Это управляет двумя потоками синхронно. Вы получаете некоторый параллелизм при выполнении операций над объектами, не являющимися общими, но это все.

java.util.concurrent предоставляет CountDownLatch, но это одноразовый объект, который идет вразрез с вашим желанием избежать выделения памяти. Причудливый CyclicBarrier делает больше.

Это не лучшее решение — хотя блокировки были не совсем тем, что вам нужно, они были частью того, чего вы хотели, и я здесь от них избавился. Невозможно посмотреть на код и легко определить, что оба потока не могут работать с объектами одновременно.

Вы можете рассмотреть возможность двойной буферизации объектов. Вам потребуется в два раза больше памяти, но проблемы с синхронизацией проще: каждый поток, по сути, имеет свой собственный набор данных для работы, и единственный раз, когда вам нужно сделать паузу, — это когда игровой поток хочет поменять местами наборы. Параллелизм максимален. Если игровой поток опаздывает, поток рендеринга просто снова рисует то, что у него есть (если вы используете GLSurfaceView), или пропускает рендеринг. (Вы можете проявить фантазию и применить тройную буферизацию для повышения пропускной способности, но это увеличит использование памяти и задержку.)

person fadden    schedule 04.06.2013
comment
processUserInput(); gameLogic(); bufferLock.block(); renderThreadHandler.post(renderTask); recycleDisplayObjects(); enqueueDisplayTree(); cycleLock.block(); bufferLock.close(); cycleLock.close(); renderThreadHandler.post(bufferSwapTask); Другой поток открывает замки после завершения каждой задачи. Этот цикл работает, поскольку цикл повторяется примерно 45 раз в секунду, но трассировка потока DDMS показывает, что потоки практически неспособны к одновременной работе, несмотря на то, что block() вызывается только в цикле одного потока. - person Boston Walker; 05.06.2013
comment
Когда я просматриваю данные профилирования метода, кажется, что DVM фактически прерывает поток рендеринга в середине метода drawAll(), чтобы он мог возобновить метод gameLogic(). Это не похоже на многопоточное поведение, которое я ищу, это функция ConditionVariables? - person Boston Walker; 05.06.2013
comment
ConditionVariable либо открыт, либо закрыт. Если он открыт, block() возвращается немедленно. Если он закрыт, block() ждет, пока его не откроет другой поток. Вот почему я использовал шаблон закрытия того, что я собираюсь заблокировать, разблокировать другой поток, а затем заблокировать. Это было бы понятнее с одноразовой защелкой, которая сбрасывается; Я как бы притворяюсь с ConditionVariable. - person fadden; 05.06.2013
comment
Пожалуйста, смотрите мой связанный вопрос. stackoverflow.com/questions/16928461 / - person Boston Walker; 05.06.2013
comment
Другой способ сделать это — использовать блокировку java.util.concurrent с включенной справедливостью. Когда вы снимаете блокировку, вы будете знать, что другой поток запустится первым, если он ждал. (Без справедливости поток может снять блокировку и немедленно повторно получить ее, даже если что-то еще ожидало, поэтому базовое ожидание/уведомление здесь не лучший вариант.) Я должен признать, что я все менее уверен, что я вполне понимаю, что вы делаете. :-) - person fadden; 05.06.2013
comment
Забавно то, что до того, как я задал этот вопрос, я попробовал точно такой же шаблон с блокировками, и параметр честности совершенно не повлиял. - person Boston Walker; 05.06.2013
comment
Хорошо, код в другом вопросе имеет смысл. bufferSwapTask должен перекрываться с processUserInput и gameLogic, а renderTask должен перекрываться с recycleDisplayObjects и enqueueDisplayTree. - person fadden; 05.06.2013