65.9K
CodeProject 正在变化。 阅读更多。
Home

跨计算机读/写文件锁

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.20/5 (4投票s)

2010年1月4日

CPOL

5分钟阅读

viewsIcon

36307

downloadIcon

522

使用文件管理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 崩溃时应用写锁而导致读取器无限期锁定
© . All rights reserved.