Inter Computer Read/Write File Lock
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.
{
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
.
/*!
\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 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 pathxi_bInitialLock
- If the resource is initially attempted to be lockedxi_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.
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 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.
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;
}
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.
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