Quick Links:

bal | bbl | bdl | bsl

Namespaces

Component bslmt_readerwriterlock
[Package bslmt]

Provide a multi-reader/single-writer lock. More...

Namespaces

namespace  bslmt

Detailed Description

Outline
Purpose:
Provide a multi-reader/single-writer lock.
Classes:
bslmt::ReaderWriterLock multi-reader/single-writer lock class
See also:
Component bslmt_readerwritermutex, Component bslmt_readlockguard, Component bslmt_writelockguard, Component bslmt_readerwriterlockassert
Description:
This component defines an efficient multi-reader/single-writer lock (RW-Lock) mechanism, bslmt::ReaderWriterLock. It is designed to allow concurrent read access to a shared resource while still controlling write access.
RW-Locks are generally used for resources which are frequently read and less frequently updated. Unlike other lock mechanisms (e.g.,"Mutexes"), RW-Locks provide two distinct but mutually exclusive lock states: a read lock state, and a write lock state. Multiple callers can simultaneously acquire a read lock, but only one write lock may be active at any given time.
This implementation gives preference to writers, which can lead to reader "starvation" in applications with continuous writes.
Comparison between bslmt Reader-Writer Locks:
  • bslmt::ReaderWriterLock (defined in this component). Preferred only when very long hold times are anticipated. Provides upgrade* methods from a locked-for-read state to a locked-for-write state, but the use of this feature is discouraged as it has performed poorly on benchmarks.
  • bslmt::ReaderWriterMutex: Preferred for most use-cases. Doesn't offer the upgrade function, but performs better than bslmt::ReaderWriterLock under most conditions.
  • bslmt::RWMutex: Deprecated.
Note that for extremely short hold times and very high concurrency, a bslmt::Mutex might outperform all of the above.
Also note that lock guards are provide by bslmt_readlockguard and bslmt_writelockguard.
Also note that asserts to verify locking are provided by bslmt_readerwriterlockassert.
Read and Write Locks:
If a read lock is attempted while another read lock is already active and there are no pending write locks, the lock will be immediately granted. If there are pending or active write locks, the reader will block until all write locks (including those acquired after the reader) are released.
bslmt::ReaderWriterLock also supports atomic conversion from read to write locks. This feature allows callers to first acquire a read lock, determine if a write operation needs to be performed, and conditionally upgrade to a write lock without possibility of another writer having changed the state of the resource.
The component supports both optimistic and pessimistic lock conversions.
Optimistic Lock Conversions:
Any basic read lock can be converted to a write lock, but the conversion is not guaranteed to be atomic. If the conversion cannot be performed atomically, which means the lock was first released, then a lock for write was acquired again (possibly after other threads have obtained and released a write lock themselves), the state of the resource must be re-evaluated, since the resource may have been changed by another thread.
Pessimistic Lock Conversions:
For conditions with high probably for write contention, or where the cost of re-evaluating the update condition is too high, clients may choose to acquire a read lock that is guaranteed to upgrade atomically, that is without the possibility of another thread acquiring a read or write lock in the meantime. The lockReadReserveWrite method allows a caller to acquire a read lock and simultaneously reserve a write lock. The read lock can then be atomically upgraded to the reserved write lock by calling the upgradeToWriteLock method.
Usage:
The following snippet of code demonstrates a typical use of a reader/writer lock. The sample implements a simple cache mechanism for user information. We expect that the information is read very frequently, but only modified when a user "badges" in or out, which should be relatively infrequent.
  struct UserInfo{
      long               d_UserId;
      char               d_UserName[MAX_USER_NAME];
      char               d_badge_location[MAX_BADGE_LOCATION];
      int                d_inOutStatus;
      bsls::TimeInterval d_badgeTime;
  };

  class UserInfoCache {
      typedef bsl::map<int, UserInfo> InfoMap;

      bslmt::ReaderWriterLock d_lock;
      InfoMap                 d_infoMap;

    public:
      UserInfoCache();
      ~UserInfoCache();

      int getUserInfo(int userId, UserInfo *userInfo);
      int updateUserInfo(int userId, UserInfo *userInfo);
      int addUserInfo(int userId, UserInfo *userInfo);
      void removeUser(int userId);
  };

  inline
  UserInfoCache::UserInfoCache()
  {
  }

  inline
  UserInfoCache::~UserInfoCache()
  {
  }

  inline
  int UserInfoCache::getUserInfo(int userId, UserInfo *userInfo)
  {
      int ret = 1;
Getting the user info does not require any write access. We do, however, need read access to d_infoMap, which is controlled by d_lock. (Note that writers will block until this read lock is released, but concurrent reads are allowed.) The user info is copied into the caller-owned location userInfo.
      d_lock.lockRead();
      InfoMap::iterator it = d_infoMap.find(userId);
      if (d_infoMap.end() != it) {
          *userInfo = it->second;
          ret = 0;
      }
      d_lock.unlock();
      return ret;
  }

  inline
  int UserInfoCache::updateUserInfo(int userId, UserInfo *userInfo)
  {
      int ret = 1;
Although we intend to update the information, we first acquire a read lock to locate the item. This allows other threads to read the list while we find the item. If we do not locate the item we can simply release the read lock and return an error without causing any other reading thread to block. (Again, other writers will block until this read lock is released.)
      d_lock.lockRead();
      InfoMap::iterator it = d_infoMap.find(userId);
      if (d_infoMap.end() != it) {
Since it != end(), we found the item. Now we need to upgrade to a write lock. If we can't do this atomically, then we need to locate the item again. This is because another thread may have changed d_infoMap during the time between our read and write locks.
          if (d_lock.upgradeToWriteLock()) {
              it = d_infoMap.find(userId);
          }
This is a little more costly, but since we don't expect many concurrent writes, it should not happen often. In the (likely) event that we do upgrade to a write lock atomically, then the second lookup above is not performed. In any case, we can now update the information and release the lock, since we already have a pointer to the item and we know that the list could not have been changed by anyone else.
          if (d_infoMap.end() != it) {
              it->second = *userInfo;
              ret = 0;
          }
          d_lock.unlock();
      }
      else {
          d_lock.unlock();
      }
      return ret;
  }

  inline
  int UserInfoCache::addUserInfo(int userId, UserInfo *userInfo)
  {
      d_lock.lockRead();
      bool found = !! d_infoMap.count(userId);
      if (! found) {
          if (d_lock.upgradeToWriteLock()) {
              found = !! d_infoMap.count(userId);
          }
          if (! found) {
              d_infoMap[userId] = *userInfo;
          }
          d_lock.unlock();
      }
      else {
          d_lock.unlock();
      }
      return found ? 1 : 0;
  }

  inline
  void UserInfoCache::removeUser(int userId)
  {
      d_lock.lockWrite();
      d_infoMap.erase(userId);
      d_lock.unlock();
  }