5

I was wondering what are the advantages of using a upgradable read lock as opposed performing these steps:

  1. Take read lock
  2. Check condition to see if we need to take write lock
  3. Release Read Lock
  4. Take Write Lock
  5. Perform update
  6. Release Write Lock

One apparent disadvantage of the performing the above steps as opposed taking an upgradable read lock is, that there is an window of time between the steps 3 and 4, where another thread can take up a write lock.

Apart from this advantage what other advantages do you find for taking upgradable read lock over the steps I have mentioned above?

coder_bro
  • 10,503
  • 13
  • 56
  • 88

2 Answers2

7

Let's consider the different ways in which one can use a reader-writer lock that doesn't have a separate "upgradable reader".

With your pattern, there is a race between step 3 and 4 as you point out, where another thread can take the writer lock. More to the point, there is a step between 3 and 4 where a thread can take the writer lock and change the state we observed in step 2.

Therefore, we've got four choices depending on how likey this is to happen:

  1. We stay with your approach because this is actually impossible (e.g. a given state transition is one-way in our application, so once observed it is permanent). In this case though we could quite possibly have remodelled so as to not need a lock at all. (One-way transitions lend themselves to lock-free techniques).

  2. We just take the writer lock in the first place, because the state we observe in step 2 is very likely to change and it's a waste of time checking it with a reader lock.

  3. We change your steps to:

    1. Take read lock
    2. Check condition to see if we need to take write lock
    3. Release Read Lock
    4. Take Write Lock
    5. Re-check the condition in case it changed.
    6. Perform update
    7. Release Write Lock
  4. We change to:

    1. Take a read lock on a recursion-supporting lock.
    2. Check to see if we need to take write lock.
    3. Take write lock (no release of read).
    4. Perform update.
    5. Release write lock.
    6. Release read lock.

It's not hard to see why 4 was more tempting to some, though it is only slightly harder to see how it makes deadlocks easy to create. Sadly, that slightly harder is just enough for a lot of people to see the advantages without seeing the disadvantages.

For anyone who doesn't spot it, if two threads have a read lock and one of them upgrades to a write lock it must wait for the other to release the read-lock. However if that second thread upgrades to a write lock without releasing the read-lock then it will wait forever on the first thread, which will wait forever on it.


As said above, just which approach is best depends on how likely it is for the state to change in the meantime (or how promptly we want to react to it, I suppose). Even the last approach with the non-releasing upgrade can have its place in viable code, as long as there can only ever be one thread that ever tries to upgrade its lock without releasing.

Aside from the special case where the last option works, the difference between the other options are all about performance, and which is most performant mostly depends on the cost of re-checking the state and the likelihood of the write being aborted due to a change in the meantime.

However, note that all of them involve taking a writer lock, so all of them have the effect of blocking all reading threads, even when the write is indeed aborted.

Upgradable read-locks give us a middle-ground because while they block write-locks and other upgradable read-locks, they don't block read locks. They are perhaps better though of not as read-locks that can upgrade as a write-locks that have yet to commit to writing.* In the cases where it was decided not to upgrade, the effect on the reading threads is nil.

This means that if it's even slightly possible that the thread will decide not to change the state, the reading threads are not affected, and the performance improvement can certainly justify it's use.

*For that matter, "reader-writer" is a bit of a misnomer, we could e.g. protect an array of ints or objects with a ReaderWriterLockSlim, use the read-locks for both reading and writing individual items atomically and use the write-locks for operations that need to read the entire array without parts of it changing as it reads. In such a case it's a reading operation than needs the exclusive lock, while writing operations are fine with the shared lock.

Jon Hanna
  • 110,372
  • 10
  • 146
  • 251
0

It also prevents deadlocks that may happen because different threads operate at the same time and they wait for each other to release the locks.

linkerro
  • 5,318
  • 3
  • 25
  • 29
  • 2
    Can you please provide an example for this? Also cant we not say the same thing about the steps I have indicate above? – coder_bro Jan 09 '12 at 11:39
  • An upgrade lock does not have the race condition that you mentioned. – linkerro Jan 09 '12 at 13:44
  • There is no race condition, as they are vying for the same resource (lets say r1), only one wins and there is no deadlock. I dont see who it then prevent deadlocks? – coder_bro Jan 10 '12 at 06:37
  • It has a race, because something can change the state observed in step 2. It's an easy to handle race though, we just re-check. It can't lead to deadlocks, but another common approach to the race could, and this was a motivation behind `ReaderWriterLockSlim` being promoted as a replacement to `ReaderWriterLock`. – Jon Hanna Jan 10 '12 at 17:10
  • Ok I misunderstood what was meant by "race condition", but as Jon mentioned it does not lead to deadlocks. And great points Jon, Thanks – coder_bro Jan 11 '12 at 05:48