Windows上锁的实现

最近研究一下Windows平台上各种线程同步手段的锁的实现,比较它们的效率。根据任务的耗时、编译器的开关、线程的数量、循环的次数、CPU的规格,结果有很大差异,这不是一个权威的评测。仅仅是想实现各种锁,并亲自体会一下各自的差异。

《windows核心编程》里面提到过几种用户模式下的线程同步效果,总体来说,内核模式下的线程同步手段,比如信号、互斥量的耗时比用户态高2个数量级,一般不考虑用它们实现锁,除非是了为了跨进程的同步。

class CSLock {
public:
  CSLock() { InitializeCriticalSection(&m_cs); }
  virtual ~CSLock() { DeleteCriticalSection(&m_cs); }

  void Lock() { EnterCriticalSection(&m_cs); }
  void UnLock() { LeaveCriticalSection(&m_cs); }

private:
  CRITICAL_SECTION m_cs;

  // DISALLOW_COPY_AND_ASSIGN
  CSLock(const CSLock &) = delete;
  CSLock &operator=(const CSLock &) = delete;
};

最常见的是用关键段CRITICAL_SECTION去实现,性能中规中矩。在保护耗时小代码性能相对较差,保护耗时大代码基本跟其他持平。所以以它为评测的基准。

通常一个优化是用带自旋锁的关键段。它在尝试得到关键段之前,会自旋一些次数。微软自己的堆内存管理器是设置自旋4000次。

class CSSpinLock {
public:
  CSSpinLock() { InitializeCriticalSectionAndSpinCount(&m_cs, 4000); }
  virtual ~CSSpinLock() { DeleteCriticalSection(&m_cs); }

  void Lock() { EnterCriticalSection(&m_cs); }
  void UnLock() { LeaveCriticalSection(&m_cs); }

private:
  CRITICAL_SECTION m_cs;

  // DISALLOW_COPY_AND_ASSIGN
  CSSpinLock(const CSSpinLock &) = delete;
  CSSpinLock &operator=(const CSSpinLock &) = delete;
};

根据我自测的一些结果来看,CSLock、CSSpinLock的效率几乎差不多,某些情况下CSSpinLock可能稍快10%~20%。但是选择自旋的次数,是个玄学,姑且顺大流都是用4000次吧。

此外我们还可以使用c++标准库的std::mutex

class StdMutexLock {
public:
  StdMutexLock() {}
  virtual ~StdMutexLock() {}

  void Lock() { mutex_.lock(); }
  void UnLock() { mutex_.unlock(); }

private:
  std::mutex mutex_;

  // DISALLOW_COPY_AND_ASSIGN
  StdMutexLock(const StdMutexLock &) = delete;
  StdMutexLock &operator=(const StdMutexLock &) = delete;
};

大多数情况下,std::mutex性能是优于CSLock、CSSpinLock的,耗时小代码情况下甚至比CSLock快5倍以上。

还有Slim读写锁,在保护耗时小代码的情况下,它的性能比CSLock快10倍。

class SrwLock {
public:
  SrwLock() {}
  virtual ~SrwLock() {}

  void Lock() {
    ::AcquireSRWLockExclusive(reinterpret_cast<PSRWLOCK>(&m_lock));
  }
  void UnLock() {
    ::ReleaseSRWLockExclusive(reinterpret_cast<PSRWLOCK>(&m_lock));
  }

private:
  SRWLOCK m_lock = SRWLOCK_INIT;

  // DISALLOW_COPY_AND_ASSIGN
  SrwLock(const SrwLock &) = delete;
  SrwLock &operator=(const SrwLock &) = delete;
};

有个真正的自旋锁,代码我是从chromium项目里扒出来的:

class SpinLock {
public:
  SpinLock() = default;
  virtual ~SpinLock() = default;

  void Lock() override {
    if ((!lock_.exchange(true, std::memory_order_acquire)))
      return;
    LockSlow();
  }
  void UnLock() override { lock_.store(false, std::memory_order_release); }
  std::string name() override { return std::string("SpinLock"); }

private:
  void LockSlow() {
    static const int kYieldProcessorTries = 1000;
    static const int kYieldThreadTries = 10;
    int yield_thread_count = 0;
    do {
      do {
        for (int count = 0; count < kYieldProcessorTries; ++count) {
          YieldProcessor();
          if (!lock_.load(std::memory_order_relaxed) &&
              !lock_.exchange(true, std::memory_order_acquire))
            return;
        }

        if (yield_thread_count < kYieldThreadTries) {
          ++yield_thread_count;
          SwitchToThread();
        } else {
          ::Sleep(1);
        }
      } while (lock_.load(std::memory_order_relaxed));
    } while (lock_.exchange(true, std::memory_order_acquire));
  }

  std::atomic<bool> lock_ = {0};
};

SpinLock的性能居中,一般比CSLock快3倍。

此外对于简单的数据同步修改,还可以用windows原子锁Interlocked系列API,还有c++标准库的std::atomic系列。

因为数据同步修改都是耗时小代码,所以直接跟保护耗时小代码性能最好的SrwLock比较。三者差异通常不大,根据不同条件有的会好一些,有的会差一些,竟然某些情况下SrwLock反而更优。

因此,为了代码的通用性,一般任务就使用std::mutex,保护数据就使用std::atomic,它们的性能通常都不错的。如果要追求极致性能就用SrwLock。至于关键段CRITICAL_SECTION实现的锁,通常不是最好的选择。