跨计算机读/写文件锁
使用文件管理Windows API实现跨计算机读/写锁
引言
有时需要从位于不同计算机上的多个应用程序访问某个共享资源。例如,可能存在两个应用程序访问位于网络共享上的同一个 Microsoft Access 数据库文件。在某个时刻,其中一个应用程序需要压缩数据库文件,这涉及到将文件复制到另一个名称,删除原始文件,然后将新压缩的文件重命名为原始文件名。在此过程中,访问数据库的所有线程和进程都应等待,否则它们将失败。另一方面,启动数据库文件压缩的线程应该等待所有其他线程和进程完成其数据库工作。存在大量的同步机制,如命名事件、命名互斥体,它们允许进程间同步。但我不知道有任何机制允许跨计算机同步。我尝试在互联网上搜索任何现有的解决方案,但一无所获。这就是为什么我决定创建一种可以使用文件管理Windows API实现的跨计算机读/写锁。
Using the Code
锁类旨在用作 MFC CSingleLock
类的用途。它们不是线程安全的,因为它们旨在创建在线程堆栈上,并且通常它们的生命周期是在访问共享资源的范围内。
{
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)
// ...
}
第一个参数是要访问的资源的路径。第二个参数是构造函数是否尝试进行初始锁定。应用程序应该对资源所在的目录具有读/写访问权限,因为这些锁会在同一目录中创建/打开两个锁文件。
代码工作原理
该实现基于 CreateFile
可以被视为原子操作的事实。有两个文件用于锁定。第一个带有 .rlc 扩展名的文件由读取器以共享模式创建/打开,允许多个读取器打开文件,而写入器以独占模式创建/打开此文件,只允许单个写入器访问此文件。第二个带有 .wlc 扩展名的文件由读取器以共享模式创建/打开然后立即关闭,而写入器以独占模式创建/打开此文件以防止读取器进入。这允许写入器在访问资源时获得优先权,并防止写入器饿死。如果读取器/写入器由于共享冲突错误而无法创建/打开某些锁文件,它将等待一段时间并再次尝试访问文件。这允许等待锁定直到资源解锁。
CReadFileLock
和 CWriteFileLock
类实际上不过是使用基类实现 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) {}
};
其中 CRWFileLock
是实际的实现类。
/*!
\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;
};
CRWFileLock
类的构造函数接受以下参数
xi_bIsReadLock
- 要创建的锁的类型(读锁为true
,写锁为false
)xi_cszFilePath
- 要锁定的资源的路径。此文件不一定需要存在,因为此路径仅用于创建带有 .rlc 和 .wlc 扩展名的两个锁文件的路径,这些路径会附加到原始路径xi_bInitialLock
- 如果资源初始尝试锁定xi_nPollPeriodMs
- 在尝试再次访问锁文件时用于睡眠的时间
构造函数的代码只是初始化类成员变量,并在指定初始锁时调用 Lock 操作。
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();
}
}
析构函数只是调用 Unlock 操作,以便在退出作用域时进行自动解锁。
NMt::CRWFileLock::~CRWFileLock()
{
Unlock();
}
锁类有两个通用操作:Lock
和 Unlock
。
但是,检查锁是否已应用的通用代码在读锁和写锁之间是不同的。
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;
}
相反,写入器首先尝试创建写入器锁文件。如果由于共享冲突错误而失败,则表明有其他写入器创建了此文件,因此写入器会睡眠一段时间,然后再次尝试创建此文件。成功创建写入器锁文件后,写入器将继续创建/打开读取器/写入器锁文件。它尝试以独占模式创建此文件,以确保只有一个写入器可以访问共享资源。如果由于共享冲突错误而失败,则表明一个/多个读取器或一个写入器打开了此文件。在这种情况下,写入器会睡眠一段时间,然后再次尝试打开此文件。
Unlock 操作只是关闭锁文件的句柄,允许其他线程/进程访问这些文件。
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;
}
历史
- 2009/12/31:初始版本
- 2014/04/01:修复了一个错误,以防止在 Windows OS 崩溃时应用写锁而导致读取器无限期锁定