Writing while reads pile up

3

I had a fun time speaking at the No Fluff Just Stuff Gateway Software Symposium on Friday. The home town show is always fun as there are so many friends in the crowd. Wish I could have been there Saturday and Sunday instead of in airports and planes but family business intervened.

I did have a question in one of my concurrency talks that I wanted to follow up on. The question was in the context of ReentrantReadWriteLock and whether you could use some of the extra methods in it (like getQueueLength()) to have a writing thread continue processing until some number of reader threads have piled up in need of a read. Or at least that’s how I remember the question. :)

I actually hacked something like this together just for fun in the airport yesterday. Basically I’m using a RRWL to protect access to some shared data and I have both readers and writers. When a writer is writing it actually uses the current queue length of the RRWL to decide when it should give it up:

lock.writeLock().lock();
try {
while(lock.getQueueLength() < MAX_WAITING_THREADS) {
// process a work item
}
} finally {
lock.writeLock().unlock();
}

This does work and is possibly interesting but it's not something I'd recommend. First, getQueueLength() is a combination of readers *and* writers waiting for the lock and there's no way to tell which. Maybe that doesn't matter though - maybe any kind of waiter accumulating is a good reason to release the lock.

The javadoc however states that this method:

Returns an estimate of the number of threads waiting to acquire either the read or write lock. The value is only an estimate because the number of threads may change dynamically while this method traverses internal data structures. This method is designed for use in monitoring of the system state, not for synchronization control.

So the value not be accurate and is not designed for synchronization control. A wise man should probably heed this advice. Its repeated in a different form in the class docs as well.

I then hacked together a different solution that doesn’t rely on the lock itself but rather uses a separate resettable count of the number of waiting readers (I made it specific to readers in this case):

class ResettableLatch {
private final int count;
private AtomicInteger waiting = new AtomicInteger(0);

public ResettableLatch(int count) {
this.count = count;
}

public void readerWaiting() {
waiting.incrementAndGet();
}

public int waitingCount() {
return waiting.get();
}

public boolean shouldRelease() {
return waiting.get() >= count;
}

public void reset() {
waiting.set(0);
}
}

To use this latch I made readers indicate they were waiting before they tried to acquire the lock:

latch.readerWaiting();
lock.readLock().lock();

And then I wrote a processing loop that watched the latch in the writer:

lock.writeLock().lock();
try {
latch.reset();
while(! latch.shouldRelease()) {
// process a work item
}
} finally {
lock.writeLock().unlock();
}

The latch state is being updated by readers regardless of anyone is waiting but when the writer enters it resets the state. There is a small race there where readers that start reading between the write lock acquire and the latch reset and lost. That means I may lose waiters which in some circumstances could be an issue. Then I process in a loop until the latch says I’ve got too many readers piled up and waiting.

I can think of a couple other ways to potentially do this. I’d be interested in hearing other people’s thoughts as well.

Comments

3 Responses to “Writing while reads pile up”
  1. IMHO, the rule of thumb is if you have two calls it is not atomic operation (i.e. latch.readerWaiting() and lock.readLock().lock();). I wonder if it could be made into an one call with custom subclass of AbstractQueuedSynchronizer. The latter is a quite interesting topic on its own as if it is not widely used.

  2. Alex says:

    Yeah, those two calls are obviously not atomic. I’m less concerned about that race than the one on the writer side. One of my other thoughts was about doing exactly what you say and utilizing AbstractQueuedSynchronizer to put that atomicity together. I think that’s definitely one possible answer.

  3. Jeff Grigg says:

    My question on Friday was regarding the potential for a sequential set of overlapping readers locking out a waiting writer forever. It seems that this could happen, as the JavaDoc explicitly says, “A nonfair lock that is continously contended may indefinitely postpone one or more reader or writer threads”.

    The scenario I had in mind would work like this:
    1. Reader 1 gets a read lock.
    2. The writer requests a write lock, and is blocked.
    3. Reader 2 requests a read lock — and let’s say they get it.
    4. Reader 1 releases their read lock.
    5. Reader 3 requests a read lock — and let’s say they get it.
    6. Reader 2 releases their read lock.

    As long as steps 5 and 6 are repeated as shown above, the writer would be delayed indefinately.

    However, in writing tests for this, I’m finding that the ‘java.util.concurrent.locks.ReentrantReadWriteLock’ class has the (apparently undocumented) behavior that it stops granting read locks when a write lock is requested. So it appears that even in non-fair mode, readers will NOT block writers indefinitely. (…as long as every lock holder releases their lock within a reasonable period of time!)

    What I find is that in step 3 above, Reader 2 is delayed until the writer gets, uses and releases the write lock. So the write does NOT get indefinitely postponed in this scenario.

Speak Your Mind

Tell us what you're thinking...
and oh, if you want a pic to show with your comment, go get a gravatar!