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);
}
{
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 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.
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 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,
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 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)
{
if (0 == _access(m_sWriterLockFilePath, 0))
{
Sleep(m_nPollPeriodMs);
continue;
}
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,
DELETE,
0, NULL, 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)
{
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_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 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) {
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