Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

Inter Computer Read/Write File Lock

4.20/5 (4 votes)
4 Jan 2010CPOL5 min read 35.7K   502  
Using File Management Windows API for implementing inter computer Read/Write lock

Introduction

Sometimes it is required to access some shared resource from several applications which reside on different computers. For example, the case can be that there are two applications that access the same Microsoft Access database file which is located on the network share. At some moment, one of the applications needs to compact the database file which involves copying the file to a different name, removing the original file and than renaming the new compacted file to the original. During this process, all threads and processes that access the database should wait, otherwise they fail. On the other hand, the thread that starts compacting the database file should wait for all other threads and processes to finish their work with the database. There are plenty of synchronization mechanisms like named events, named mutexes that allow inter process synchronization. But I don't know any that allows synchronization between computers. I tried to search for any existing solutions to this problem in the Internet and found nothing. That is why I decided to create an Inter Computer Read/Write Lock which can be implemented using File Management Windows API.

Using the Code

The lock classes are intended to be used like MFC CSingleLock class. They are not thread-safe because they are intended to be created on a thread stack and usually their lifetime is inside the scope where the shared resource is accessed.

C++
{
    NMt::CReadFileLock l_Lock("\\\\mycomputer\\share\\test.mdb", true);
    // connect to db and perform read/update operations
    // ...
    // close db connection
}

{
    NMt::CWriteFileLock l_Lock("\\\\mycomputer\\share\\test.mdb", true);
    // compact db
    // (copies db to a different file while compacting it, removes the original db file, 
    // renames the compacted file to the original)
    // ...
} 

The first parameter is a path to a resource that should be accessed. The second parameter is whether it attempts to lock initially in the constructor. An application should have read/write access to a directory where the resource is located because these locks create/open two lock files in the same directory.

How the Code Works

The implementation is based on the fact that CreateFile can be viewed as an atomic operation. There are two files used for locking. The first one with .rlc extension is created/opened by readers in the shared mode, allowing many readers to open the file while writers create/open this file in the exclusive mode allowing only a single writer to access this file. The second file with .wlc extension is created/opened by readers in the shared mode and immediately closed, while writers create/open this file in the exclusive mode to prevent readers from entering. This allows writers to get preference when accessing the resource and prevents writers from starvation. If a reader/writer fails to create/open some of the lock files because of the sharing violation error, it will wait for some period and try to access the files again. This allows waiting on the lock until resource is unlocked.

The CReadFileLock and CWriteFileLock classes are actually not more than a convenient way to use the base class implementation - CRWFileLock.

C++
/*!
\brief Read File Lock class.
*/
class CReadFileLock : public CRWFileLock
{
public:
  // LIFECYCLE
  /*!
  \brief Constructor.
  \param[in] xi_cszFilePath a path to a file to be accessed
  \param[in] xi_bInitialLock if it is initially locked
  \param[in] xi_nPollPeriodMs polling period (milliseconds)
  */
  CReadFileLock(LPCTSTR xi_cszFilePath,
                bool xi_bInitialLock = false,
                DWORD xi_nPollPeriodMs = 1000) :
      CRWFileLock(true, xi_cszFilePath, xi_bInitialLock, xi_nPollPeriodMs) {}
};
/*!
\brief Write File Lock class.
*/
class CWriteFileLock : public CRWFileLock
{
public:
  // LIFECYCLE
  /*!
  \brief Constructor.
  \param[in] xi_cszFilePath a path to a file to be accessed
  \param[in] xi_bInitialLock if it is initially locked
  \param[in] xi_nPollPeriodMs polling period (milliseconds)
  */
  CWriteFileLock(LPCTSTR xi_cszFilePath,
                 bool xi_bInitialLock = false,
                 DWORD xi_nPollPeriodMs = 1000) :
      CRWFileLock(false, xi_cszFilePath, xi_bInitialLock, xi_nPollPeriodMs) {}
};

Where CRWFileLock is an actual implementation class.

C++
/*!
\brief A Read/Write File Lock implementation base class.
*/
class CRWFileLock
{
public:
  // LIFECYCLE
  /*!
  \brief Constructor.
  \param[in] xi_bIsReadLock if it is a read lock
  \param[in] xi_cszFilePath a path to a file to be accessed
  \param[in] xi_bInitialLock if it is initially locked
  \param[in] xi_nPollPeriodMs polling period (milliseconds)
  */
  CRWFileLock(bool xi_bIsReadLock,
              LPCTSTR xi_cszFilePath,
              bool xi_bInitialLock = false,
              DWORD xi_nPollPeriodMs = 1000);
  /*!
  \brief Destructor.
  */
  ~CRWFileLock();
  // OPERATIONS
  /*!
  \brief Locks access to the file.
  */
  void Lock();
  /*!
  \brief Unlocks access to the file.
  */
  void Unlock();
protected:
  // DATA MEMBERS
  /*!
  \brief Readers/Writers lock file path.
  */
  CString m_sReaderWriterLockFilePath;
  /*!
  \brief Writers lock file path.
  */
  CString m_sWriterLockFilePath;
  /*!
  \brief Readers/Writers lock file.
  */
  HANDLE m_hReaderWriterLockFile;
  /*!
  \brief Writers lock file.
  */
  HANDLE m_hWriterLockFile;
  /*!
  \brief If it is locked.
  */
  bool m_bIsLocked;
  /*!
  \brief If it is a read lock.
  */
  bool m_bIsReadLock;
  /*!
  \brief Polling period (milliseconds).
  */
  DWORD m_nPollPeriodMs;
};

The constructor of CRWFileLock class accepts the following parameters:

  • xi_bIsReadLock - A type of the lock to be created (true for a read lock, false for a write lock)
  • xi_cszFilePath - A path to a resource to be locked. This file is not necessary to exist because this path is just used to create paths for two lock files with extensions .rlc and .wlc which are added to the original path
  • xi_bInitialLock - If the resource is initially attempted to be locked
  • xi_nPollPeriodMs - Used for how much time to sleep when trying to access lock the files again

The code of the constructor just initialises the class member variables and calls Lock operation if an initial lock is specified.

C++
NMt::CRWFileLock::CRWFileLock(bool xi_bIsReadLock,
                              LPCTSTR xi_cszFilePath, 
                              bool xi_bInitialLock/* = false*/, 
                              DWORD xi_nPollPeriodMs/* = 1000*/) :
  m_bIsReadLock(xi_bIsReadLock), m_bIsLocked(false),
  m_hReaderWriterLockFile(0), m_hWriterLockFile(0), 
  m_nPollPeriodMs(xi_nPollPeriodMs)
{
  CString l_sFilePath = xi_cszFilePath;
  if (!l_sFilePath.IsEmpty())
  {
    m_sReaderWriterLockFilePath = l_sFilePath + ".rlc";
    m_sWriterLockFilePath = l_sFilePath + ".wlc";
  }
  if (xi_bInitialLock)
  {
    Lock();
  }
}

The destructor just calls Unlock operation to allow automatic unlocking when exiting a scope.

C++
NMt::CRWFileLock::~CRWFileLock()
{
  Unlock();
} 

There are two operations common for lock classes: Lock and Unlock.

But the common code at the beginning that checks if the lock is already applied the lock operation differs for a read lock and a write lock.

C++
void
NMt::CRWFileLock::Lock()
{
  if (m_sReaderWriterLockFilePath.IsEmpty() || m_bIsLocked)
  {
    return;
  }
  if (m_bIsReadLock)
  {
    // prevent writers from starvation
    while (true)
    {
      // try to open in shared mode
      m_hWriterLockFile = ::CreateFile(m_sWriterLockFilePath, 
                                       GENERIC_READ, 
                                       FILE_SHARE_READ,
                                       NULL, // default security
                                       OPEN_ALWAYS,
                                       FILE_ATTRIBUTE_NORMAL,
                                       NULL);
      if (INVALID_HANDLE_VALUE == m_hWriterLockFile)
      {
        DWORD l_nErr = ::GetLastError();
        if (ERROR_SHARING_VIOLATION == l_nErr)
        {
          // locked by writer, wait
          Sleep(m_nPollPeriodMs);
          continue;
        }
        DisplayMsg("Cannot create a writer lock file %s: %d", m_sWriterLockFilePath, l_nErr);
        break;
      }
      // succeeded to open - no writers claimed access
      // close it to allow writers to open it
      if (0 == ::CloseHandle(m_hWriterLockFile))
      {
        DisplayMsg("Cannot close a writer lock file %s: %d", m_sWriterLockFilePath, ::GetLastError());
      }
      break;
    }

    while (true)
    {
      // lock writers, allow readers to share
      m_hReaderWriterLockFile = ::CreateFile(m_sReaderWriterLockFilePath, 
                                             GENERIC_READ, 
                                             FILE_SHARE_READ,
                                             NULL, // default security
                                             OPEN_ALWAYS,
                                             FILE_ATTRIBUTE_NORMAL,
                                             NULL);
      if (INVALID_HANDLE_VALUE == m_hReaderWriterLockFile)
      {
        DWORD l_nErr = ::GetLastError();
        if (ERROR_SHARING_VIOLATION == l_nErr)
        {
          // locked by writer, wait
          Sleep(m_nPollPeriodMs);
          continue;
        }
        DisplayMsg("Cannot create a reader/writer lock file %s: %d", m_sReaderWriterLockFilePath, l_nErr);
        break;
      }
      // succeeded to lock
      break;
    }
  }
  else
  {
    // prevent readers from entering, writers open this file in exclusive mode
    while (true)
    {
      m_hWriterLockFile = ::CreateFile(m_sWriterLockFilePath, 
                                       GENERIC_READ | GENERIC_WRITE, 
                                       0, // exclusive
                                       NULL, // default security
                                       OPEN_ALWAYS,
                                       FILE_ATTRIBUTE_NORMAL,
                                       NULL);
      if (INVALID_HANDLE_VALUE == m_hWriterLockFile)
      {
        DWORD l_nErr = ::GetLastError();
        if (ERROR_SHARING_VIOLATION == l_nErr)
        {
          // locked by writers, wait
          Sleep(m_nPollPeriodMs);
          continue;
        }
        DisplayMsg("Cannot create a writer lock file %s: %d", m_sWriterLockFilePath, l_nErr);
        break;
      }
      // succeeded to lock
      break;
    }

    // lock readers/writers
    while (true)
    {
      m_hReaderWriterLockFile = ::CreateFile(m_sReaderWriterLockFilePath, 
                                             GENERIC_READ | GENERIC_WRITE, 
                                             0, // exclusive access
                                             NULL, // default security
                                             OPEN_ALWAYS,
                                             FILE_ATTRIBUTE_NORMAL,
                                             NULL);
      if (INVALID_HANDLE_VALUE == m_hReaderWriterLockFile)
      {
        DWORD l_nErr = ::GetLastError();
        if (ERROR_SHARING_VIOLATION == l_nErr)
        {
          // locked by readers/writers, wait
          Sleep(m_nPollPeriodMs);
          continue;
        }
        DisplayMsg("Cannot create a reader/writer lock file %s: %d", m_sReaderWriterLockFilePath, l_nErr);
        break;
      }
      // succeeded to lock
      break;
    }

  }
  m_bIsLocked = true;
}
In the read lock, readers try to create/open the writer lock file in the shared mode. If this file is opened in the exclusive mode, readers wait for some polling period before trying to open this file again. If readers succeed to open this file, they immediately close it. This allows writers to open this file and prevents them from possible starvation. Then, readers proceed by creating/opening the common reader/writer lock file in the shared read mode. This allows multiple readers to access the resource simultaneously while preventing writers to enter because writers try to create/open this file in the exclusive mode. If a reader fails to open this file because of the sharing violation error that indicates that a writer opened this file in the exclusive mode, in this case the reader sleeps for some polling period and tries to open this file again.

On the opposite side, a writer tries first to create the writer lock file. If it fails because of the sharing violation error, it indicates that some other writer created this file, so the writer sleeps for some polling period and tries to create this file again. After successfully creating the writer lock file, the writer proceeds to creation/opening of the reader/writer lock file. It tries to create this file in the exclusive mode assuring that only a single writer can access the shared resource. If it fails because of the sharing violation error, it indicates that one/several readers or a single writer opened this file. In this case, the writer sleeps for some polling period and tries to open this file again.

Unlock operation just closes handles to the lock files allowing accessing these files from other threads/processes.

C++
void 
NMt::CRWFileLock::Unlock()
{
  if (m_sReaderWriterLockFilePath.IsEmpty() || !m_bIsLocked)
  {
    return;
  }
  if (!m_bIsReadLock) // write lock
  {
    // release readers
    if (0 == ::CloseHandle(m_hWriterLockFile))
    {
      DisplayMsg("Cannot close a writer lock file %s: %d", 
        m_sWriterLockFilePath, ::GetLastError());
    }
  }
  // release readers/writers
  if (0 == ::CloseHandle(m_hReaderWriterLockFile))
  {
    DisplayMsg("Cannot close a reader/writer lock file %s: %d", 
        m_sReaderWriterLockFilePath, ::GetLastError());
  }
  m_bIsLocked = false;
}

History

  • 12/31/2009: Initial version
  • 04/01/2014: Fixed a bug to prevent readers from infinite locking if Windows OS crashes when a write lock was applied

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)