Click here to Skip to main content
Click here to Skip to main content

Inter Computer Read/Write File Lock

By , 4 Jan 2010
 

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 allow synchronization between computers. I tried to search for some 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.

{
    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 shared mode, allowing many readers to open the file while writers create/open this file in exclusive mode allowing only a single writer to access this file. The second file with .wlc extension is tested by readers for existence to allow writers to get preference when accessing the resource, while writers create this file in exclusive mode with a special flag that instructs Windows to delete this file when all handles to it are closed. If reader/writer fails to create/open some of the lock files because of 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.

  /*!
  \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.

  /*!
  \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 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 files again

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

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.

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

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

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

 void
NMt::CRWFileLock::Lock()
{
  if (m_sReaderWriterLockFilePath.IsEmpty() || m_bIsLocked)
  {
    return;
  }
  if (m_bIsReadLock)
  {
    while (true)
    {
      // check if writer lock file exists, wait
      if (0 == _access(m_sWriterLockFilePath, 0))
      {
        Sleep(m_nPollPeriodMs);
        continue;
      }
      // 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, 
                                       DELETE, 
                                       0, // exclusive
                                       NULL, // default security
                                       OPEN_ALWAYS,
                                       FILE_ATTRIBUTE_NORMAL | 
					FILE_FLAG_DELETE_ON_CLOSE,
                                       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_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 read lock, the existence of the writer lock file is initially tested. If this file exists, readers wait for some polling period before testing this file again. This test prevents writers from possible starvation. If the writer lock file does not exist, readers proceed by creating/opening common reader/writer lock file in 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 exclusive mode. If reader fails to open this file because of the sharing violation error that indicates that writer opened this file in exclusive mode, in this case reader sleeps for some polling period and tries to open this file again.

On the opposite side, the writer tries first to create a writer lock file. If it fails because of 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. The writer lock file is created with a special flag that instructs Windows to delete this file when all handles to it are closed. It ensures proper deletion of this file when writer unlocks or if a process crashes because it is crucial to prevent readers from blocking infinitely. After successfully creating writer lock file, writer proceeds to creation/opening of reader/writer lock file. It tries to create this file in exclusive mode assuring that only single writer can access the shared resource. If it fails because of 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 lock files allowing accessing these files from other threads/processes.

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

License

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

About the Author

AndreBroz
Software Developer
Israel Israel
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
Hint: For improved responsiveness ensure Javascript is enabled and choose 'Normal' from the Layout dropdown and hit 'Update'.
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
GeneralRe: Concurrent writersmemberAndreBroz19 Sep '10 - 4:26 
GeneralRe: Concurrent writersmemberAndreBroz20 Sep '10 - 5:41 
Generaljust for helpmembersealplusplus1 Feb '10 - 19:08 
GeneralRe: just for helpmemberAndreBroz1 Feb '10 - 23:27 
Hi,
 
Sorry I do not understand what you mean. My article is not about CString management. Anyway you can see CString source code if you have Visual Studio installed, you can look in Visual Studio installation folder for VC\atlmfc\include\cstringt.h, cstringt.inl
 
Good luck
GeneralRe: just for helpmembersealplusplus2 Feb '10 - 15:50 
GeneralRe: just for helpmemberAndreBroz3 Feb '10 - 23:22 
GeneralRe: just for helpmembersealplusplus4 Feb '10 - 14:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130516.1 | Last Updated 4 Jan 2010
Article Copyright 2010 by AndreBroz
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid