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);
}
{
NMt::CWriteFileLock l_Lock("\\\\mycomputer\\share\\test.mdb", true);
}
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
.
class CReadFileLock : public CRWFileLock
{
public:
CReadFileLock(LPCTSTR xi_cszFilePath,
bool xi_bInitialLock = false,
DWORD xi_nPollPeriodMs = 1000) :
CRWFileLock(true, xi_cszFilePath, xi_bInitialLock, xi_nPollPeriodMs) {}
};
class CWriteFileLock : public CRWFileLock
{
public:
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.
class CRWFileLock
{
public:
CRWFileLock(bool xi_bIsReadLock,
LPCTSTR xi_cszFilePath,
bool xi_bInitialLock = false,
DWORD xi_nPollPeriodMs = 1000);
~CRWFileLock();
void Lock();
void Unlock();
protected:
CString m_sReaderWriterLockFilePath;
CString m_sWriterLockFilePath;
HANDLE m_hReaderWriterLockFile;
HANDLE m_hWriterLockFile;
bool m_bIsLocked;
bool m_bIsReadLock;
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.
NMt::CRWFileLock::CRWFileLock(bool xi_bIsReadLock,
LPCTSTR xi_cszFilePath,
bool xi_bInitialLock,
DWORD xi_nPollPeriodMs) :
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)
{
while (true)
{
m_hWriterLockFile = ::CreateFile(m_sWriterLockFilePath,
GENERIC_READ,
FILE_SHARE_READ,
NULL, OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (INVALID_HANDLE_VALUE == m_hWriterLockFile)
{
DWORD l_nErr = ::GetLastError();
if (ERROR_SHARING_VIOLATION == l_nErr)
{
Sleep(m_nPollPeriodMs);
continue;
}
DisplayMsg("Cannot create a writer lock file %s: %d", m_sWriterLockFilePath, l_nErr);
break;
}
if (0 == ::CloseHandle(m_hWriterLockFile))
{
DisplayMsg("Cannot close a writer lock file %s: %d", m_sWriterLockFilePath, ::GetLastError());
}
break;
}
while (true)
{
m_hReaderWriterLockFile = ::CreateFile(m_sReaderWriterLockFilePath,
GENERIC_READ,
FILE_SHARE_READ,
NULL, OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (INVALID_HANDLE_VALUE == m_hReaderWriterLockFile)
{
DWORD l_nErr = ::GetLastError();
if (ERROR_SHARING_VIOLATION == l_nErr)
{
Sleep(m_nPollPeriodMs);
continue;
}
DisplayMsg("Cannot create a reader/writer lock file %s: %d", m_sReaderWriterLockFilePath, l_nErr);
break;
}
break;
}
}
else
{
while (true)
{
m_hWriterLockFile = ::CreateFile(m_sWriterLockFilePath,
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (INVALID_HANDLE_VALUE == m_hWriterLockFile)
{
DWORD l_nErr = ::GetLastError();
if (ERROR_SHARING_VIOLATION == l_nErr)
{
Sleep(m_nPollPeriodMs);
continue;
}
DisplayMsg("Cannot create a writer lock file %s: %d", m_sWriterLockFilePath, l_nErr);
break;
}
break;
}
while (true)
{
m_hReaderWriterLockFile = ::CreateFile(m_sReaderWriterLockFilePath,
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_ALWAYS,
FILE_ATTRIBUTE_NORMAL,
NULL);
if (INVALID_HANDLE_VALUE == m_hReaderWriterLockFile)
{
DWORD l_nErr = ::GetLastError();
if (ERROR_SHARING_VIOLATION == l_nErr)
{
Sleep(m_nPollPeriodMs);
continue;
}
DisplayMsg("Cannot create a reader/writer lock file %s: %d", m_sReaderWriterLockFilePath, l_nErr);
break;
}
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.
void
NMt::CRWFileLock::Unlock()
{
if (m_sReaderWriterLockFilePath.IsEmpty() || !m_bIsLocked)
{
return;
}
if (!m_bIsReadLock) {
if (0 == ::CloseHandle(m_hWriterLockFile))
{
DisplayMsg("Cannot close a writer lock file %s: %d",
m_sWriterLockFilePath, ::GetLastError());
}
}
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