Windows下轻量级Semaphore快速信号量的实现

  最近在重写弹幕君的正式版本,打算加入多线程支持,遇到了不少同步问题。这里就其中一个线程同步的信号量性能问题开一篇日志记录一下吧~

前言

  关于信号量、互斥体和临界区等用于同步的对象,这里就不再赘述了,因为无论是网上还是书本上,关于同步的知识绝对会提到这几个熟悉的名词。关于这几个同步对象本来是没什么可以挑剔的,逻辑上和实际使用上应用多很广。但是问题是Windows下实现这些对象的方法有些过于低效,因此这篇日志就是想讨论一下如何配合一些原子操作手段而提高Windows下Semaphore等同步对象的方法。
  总所周知(我想找到这篇日志的人都会知道),Windows下Mutex、Semaphore和Event都是内核对象,在撰写代码的时候都是以HANDLE关键字来声明对象的,不同的只是在申请(创建)对象的时候调用需要的CreateXXX()方法罢了。但是每次操作内核对象,都必然花费数千个CPU周期来调度一次所有内核对象,显然在低争用的情况下是非常不值得的。而与这三者不同的是CRITICALSECTION(临界区),在进入临界区的时候,并没有马上交给内核分配,而是通过本用户线程检查是否有其他线程争用。如果不存在争,则本线程立即获取临界资源;如果临界资源已被别的线程占用了,则进入内核,让系统控制线程状态(如果有设置自旋的话,则在进入内核之前还让CPU“空转”等待资源释放)。这就临界区高效的原因了。
  临界区的高性能是出于它用户级的锁定检查,而同为同步对象的信号量和互斥体为什么不能拥有临界区的高效呢?这正是本文要探讨的内容了。

实现

  作为实现的基础,临界区的实现的设计思想完全符合我们信号量的需求,但是临界区并不等于信号量,显然不能照单全收。在我看来,临界区本质就是信号量≤1的信号量对象的特例。因此,只需简单地为临界区“加上”信号数量就可以了。

基本设计思想
FastSemaphore.Wait():
1.如果信号量>0,则跳到(4)
2.如果在自旋中信号量>0,跳到(4)
3.进入内核,等待系统调度
4.信号量-1

FastSemaphore.Post():
1.信号量+N
2.如果信号量≤0,则返回
3.释放N个内核对象

  实现原理就是这么简单,但是实际编写代码可不能按照单线程的思想实现啦。要知道,每个Semaphore将在一个或多个线程中被使用,也就是信号量作为多个线程共享对象应该保证线程安全。那么可能有人会想,直接用EnterCriticalSection()来保护整个Semaphore不久好了?–答案是否定的,因为这样做的话,Semaphore就变成了临界资源,也就是说,只能有一个线程能够在某个时刻访问这个信号量,因此当任意一个线程被信号量阻塞之后,其余的线程将再也不能访问这个信号量–也就是传说中的死锁。
  那么如果不能简单地把整个信号量保护起来,那么应该如何保证我们这个快速Semaphore的线程安全呢?–答案就是原子操作了。在实现的主要问题是,多个线程在对信号量的值检查和修改的时候很容易会读到“脏”数据,因此,使用原子的读、写、加、减等操作就能完美快速地实现这个模型了。
  下面贴一下关键代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//检查信号量
//FastSemaphore.TryWait():
for(long tmpCount = semCount;true; tmpCount = semCount){
    if(tmpCount < = 0)   //信号量不可用时
        return false;   //获取失败
    //检查信号量是否脏数据并自减信号量
    if(InterlockedCompareExchange(
       &semCount, tmpCount - 1, tmpCount) == tmpCount)
        break;  //获取成功
}
    return true;
 
//等待信号量
//FastSemaphore.Wait():
for(unsigned i = 0; i < spinCount; i++) //先自旋
    if(TryWait())
        return;
 
if(InterlockedDecrement(&semCount) < 0) //semCount--
    WaitForSingleObject(sem, INFINITE); //进入内核等待
 
//释放信号量
//FastSemaphore.Post():
//增加N个信号量
if(InterlockedExchangeAdd(&semCount, n) < 0)
        ReleaseSemaphore(sem, 1, NULL);  //只释放一次

完整代码
点击下载: FastSemaphore.h
Ps.由于代码比较短及性能原因,类函数都写成内联函数并放在头文件FastSemaphore.h中了,使用时只需包含这个头文件就可以了。

后记

  上文提到“使用临界区保护整个信号量对象”显然是不行的,但是我觉得使用临界区去保护信号量的值来达到线程安全目的倒是应该可以的。但是在我写的上一个版本,使用临界区实现的快速信号量却没有达到目的,对信号量的访问依然是不能保证一致性的。我觉得这个设计方向应该是没有问题的,大概是我的技术还远没到家才实现不了吧~

关于自旋
  自旋是一种对多处理器相当有效的机制,而在单处理器非抢占式的系统中基本上没有作用。一次只允许一个CPU执行核心代码并发性不够高,若期望核心程序在多CPU之间的并行执行,将核心分为若干相对独立的部分,不同的CPU可以同时进入和执行核心中的不同部分,实现时可以令每个相对独立的区域进行自旋。

    • adadas
    • 2015/10/02 3:58上午

    这个代码都错的离谱了。WaitForSingleObject会把信号量减一,你的sem和semCount的值不一致

  1. 暂无 Trackback



return top