1

I am trying to synchronize a pair of non-UI threads, one thread to run game logic and one thread to render, in order to execute tasks in a logical and efficient order. A constraint I imposed myself was that the entire system operate at an allocation-free steady state, so all display objects are returned and 'recycled' and therefore the two threads must maintain a sort of two-way dialog, which occurs when I call the 'swapBuffers()' method.

In pseudocode, the order of events in the game thread looks something like this:

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);
}

The render thread is chosen to do the task of swapping buffers such that the game logic thread can do tasks in the meantime that do not modify the buffers. In my code, I combine the task of swapping buffers and rendering and define it as a Runnable object which is posted to the render thread's handler. In pseudocode, that runnable looks something like this:

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

My problem is an implementation problem. I am familiar with the concepts of handlers, synchronization blocks, and ReentrantLocks. I am aware of the Lock.await() Lock.signal() methods, but I find the documentation insufficient when trying to understand how they behave when called in an iterating loop.

How does one implement ReentrantLocks to make two threads wait on each other in such a way? Please include a practical idiom in your answer if possible.

Boston Walker
  • 534
  • 1
  • 6
  • 19

1 Answers1

1

I'm not sure a Lock is what you want. You do want the current thread to have exclusive access to the objects, but once you release the lock you want to be sure the other thread gets to execute before you get the lock again.

You could, for example, use the poorly-named 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]

This operates the two threads in lockstep. You get some concurrency when doing operations on non-shared objects, but that's it.

java.util.concurrent provides CountDownLatch, but that's a one-shot object, which goes against your desire to avoid allocations. The fancy CyclicBarrier does more.

This isn't a great solution -- while locks weren't exactly what you wanted, they were part of what you wanted, and I've done away with them here. It's not possible to look at the code and easily determine that both threads can't operate on the objects simultaneously.

You may want to consider double-buffering the objects. You'd need twice as much memory, but the synchronization issues are simpler: each thread essentially has its own set of data to work on, and the only time you have to pause is when the game thread wants to swap sets. Concurrency is maximized. If the game thread is late, the render thread just draws what it has again (if you're using a GLSurfaceView) or skips rendering. (You can get fancy and triple-buffer the whole thing to improve throughput, but that increases memory usage and latency.)

fadden
  • 51,356
  • 5
  • 116
  • 166
  • `processUserInput(); gameLogic(); bufferLock.block(); renderThreadHandler.post(renderTask); recycleDisplayObjects(); enqueueDisplayTree(); cycleLock.block(); bufferLock.close(); cycleLock.close(); renderThreadHandler.post(bufferSwapTask);` The other thread opens the locks after completion of each task. This loop works, in that the loop iterates about 45 times a second, but a DDMS thread trace reveals that the threads are almost incapable of simultaneous operation, despite block() only being called in one thread's loop. – Boston Walker Jun 04 '13 at 21:40
  • When I view the method profiling data, it seems that the DVM actually interrupts the render thread in the middle of the drawAll() method such that it can resume the gameLogic() method. This doesn't seem like the multithreaded behaviour I'm looking for, is this a feature of ConditionVariables? – Boston Walker Jun 04 '13 at 21:43
  • A `ConditionVariable` is either open or closed. If it's open, `block()` returns immediately. If it's closed, `block()` waits until another thread opens it. This is why I used the pattern "close the thing I'm going to block on, unblock the other thread, then block". This would be clearer with a one-shot latch that resets; I'm sort of faking it with `ConditionVariable`. – fadden Jun 04 '13 at 22:28
  • Please see my related question. http://stackoverflow.com/questions/16928461/conditionvariable-prevents-both-threads-from-running-simultaneously – Boston Walker Jun 04 '13 at 22:33
  • Another way to go about this would be to use a `java.util.concurrent` lock with fairness enabled. When you release the lock you'll know that the other thread will get to run first if it was waiting. (Without fairness it's possible for a thread to release a lock and immediately re-acquire it even if something else was waiting, which is why the basic wait/notify isn't a great option here.) I have to admit that I'm increasingly less certain that I understand quite what you're doing though. :-) – fadden Jun 04 '13 at 22:35
  • Funny thing is that before I asked this question, I tried the exact same pattern with Locks, and the fairness parameter had absolutely no effect. – Boston Walker Jun 04 '13 at 22:43
  • Okay, the code in the other question makes sense. `bufferSwapTask` should overlap with `processUserInput` and `gameLogic`, while `renderTask` should overlap with `recycleDisplayObjects` and `enqueueDisplayTree`. – fadden Jun 04 '13 at 22:48