许多文件中都会提到 semaphores(信号量),因为在电脑科学中它是最具历史的同步机制。它可以让你陷入理论的泥淖之中,教授们则喜欢问你一些有关于信号量的疑难杂 症。你可能不容易找到一些关于 semaphores 的有用例子,但是我告诉你,它是解决各种 producer/consumer 问题的关键要素。这种问题会存有一个缓冲区,可能在同一时间内被读出数据或被写入数据。
Win32 中的一个 semaphore 可以被锁住最多 n 次,其中 n 是 semaphore 被产生时指定的。n 常常被设计用来代表“可以锁住一份资源”的线程个数,不过并非单独一个线程就不能够拥有所有的锁定。这没有什么理由可言。
这 里有一个例子,告诉你为什么你需要一个 semaphore。考虑一下某人(我称之为 Steve)的情况。他想在加州租一辆车。租车店柜台后面坐了好几位租车代理人。Steve 告诉租车代理人说他想要一部敞篷车,接待他的那位代理人往窗外一看,有三辆敞篷车可以用,于是开始写派车单。不幸的是,就那么巧,有另三个人也同时要一辆 敞篷车,而他们的租车代理人也正在做 Steve 的代理人的相同动作。现在,有四个人想租三辆车,而必然有某个人要被淘汰出局。
让我们留下这小 小的悬疑画面,并祈祷 Steve 租得到车。租车公司这边的问题是,他们不可能即时写下派车单并且马上给租车人钥匙。整个租车程序过长,长到足够让另一位代理人把同一辆车租给另一个人。这
种情况我们已经在多线程的情况下一再地看到了。如果有许多个线程正在处理相同的资源,那么必须有某些机制被用来阻止线程干扰其他线程。
如果我们尝试写一个程序解决汽车出租问题,方法之一就是为每辆车加上一个 mutex 保护之。这虽然可行,但你可能得为一家大租车公司生产成百甚至成千个 mutexes。
另一个方法就是以单一的 mutex 为所有车辆服务,或说为所有的敞篷车服务,但这样的话一次就只能有一个店员出租敞篷车。这或许可以减少店员人数,但是面对一家忙碌的出租公司,客户可能因此转移到其竞争对手那里去。
解 决之道是:首先,所有的敞篷车都被视为相同(是啊,什么时候你租车还选颜色的?),在钥匙被交到客户手上之前,唯一需要知道的就是现在有几辆车可以用。我
们可以用 semaphore 来维护这个数字,并保证不论是增加或减少其值,都是在一个不可分割的动作内完成。当 semaphore 的数值降为0时,不论什么人要租车,就得等待了。
理论可以证明,mutex 是 semaphore 的一种退化。如果你产生一个semaphore 并令最大值为 1,那就是一个 mutex。也因此,mutex 又常被称为binary semaphore。如果某个线程拥有一个 binary semaphore,那么就没有其他线程能够获得其拥有权。在 Win32 中,这两种东西的拥有权(ownership)的意义完全不同,所以它们不能够交换使用。semaphores 不像 mutexes,它并没有所谓的“wait abandoned”状态可以被其他线程侦测到。
在许多系统中,semaphores 常被使用,因为
mutexes 可能并不存在。在Win32 中
semaphores 被使用的情况就少得多,因为 mutex 存在的缘故。
产生信号量(Semaphore)要在
Win32 环境中产生一个 semaphore,必须使用
CreateSemaphore()函数调用:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName
);
参数
lpAttributes 安全属性。如果是 NULL 就表示要使用默认属性。Windows 95 忽略这一参数。
lInitialCount
semaphore 的初值。必须大于或等于 0,并且小于或等于 lMaximumCount。
lMaximumCount
Semaphore 的最大值。这也就是在同一时间内能够锁住 semaphore 之线程的最多个数。
lpName
Semaphore 的名称(一个字符串)。任何线程(或进程) 都可以根据这一名称引用到这个semaphore。这个值可以是 NULL,意思是产生一个没有名字的 semaphore。
返回值
如果成功就传回一个 handle,否则传回 NULL。不论哪一种情况,GetLastError() 都会传回一个合理的结果。如果指定的 semaphore 名称已经存在,则该函数还是成功的,GetLastError() 会传回 ERROR_ALREADY_EXISTS。
获得锁定
Semaphore 的各个相关术语,其晦涩比起 mutexes 真是有过之而无不及。首先请你了解,semaphore 的现值代表的意义是目前可用的资源数。如果semaphore 的现值为 1,表示还有一个锁定动作可以成功。如果现值为 5,就表示还有五个锁定动作可以成功。
每当一个锁定动作成功,semaphore 的现值就会减 1。你可以使用任何一种 Wait...() 函数(例如 WaitForSingleObject())要求锁定一个 semaphore。因此,如果 semaphore 的现值不为 0,Wait...()
函数会立刻返回。这和 mutex 很像,如果没有任何线程拥有
mutex,Wait...() 会立刻返回。
如果锁 定成功,你也不会收到 semaphore 的拥有权。因为可以有一个以上的线程同时锁定一个 semaphore,所以谈 semaphore 的拥有权并没有太多帮助。在 semaphore 身上并没有所谓“独占锁定”这种事情。也因为没有拥有权的观念,一个线程可以反复调用 Wait...() 函数以产生新的锁定。这和 mutex绝不相同:拥有 mutex 的线程不论再调用多少次 Wait...() 函数,也不会被阻塞住。
一旦 semaphore 的现值降到 0,就表示资源已经耗尽。此时,任何线程如果调用 Wait...() 函数,必然要等待,直到某个锁定被解除为止。
解除锁定(Releasing Locks)
为了解除锁定,你必须调用 ReleaseSemaphore()。这个函数将 semaphore 的现值增加一个定额,通常是 1,并传回 semaphore 的前一个现值。
ReleaseSemaphore() 和 ReleaseMutex() 旗鼓相当。当你调用WaitForSingleObject()
并获得一个 semaphore 锁定之后,你就需要调用ReleaseSemaphore()。Semaphore 常常被用来保护固定大小的环状缓冲区(ring buffer)。程序如果要读取环状缓冲区的内容,必须等待 semaphore。
线程将数据写入环状缓冲区,写入的数据可能不只一笔,在这种情况下解除锁定时的
semaphore 增额应该等于写入的数据笔数。
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
参数
hSemaphore
Semaphore 的
handle。
lReleaseCount
Semaphore 现值的增额。该值不可以是负值或 0。
lpPreviousCount
藉此传回 semaphore 原来的现值。
返回值
如果成功,则传回TRUE。否则传回 FALSE。失败时可调用 GetLastError()获得原因。
ReleaseSemaphore() 对于 semaphore 所造成的现值的增加,绝对不会超过CreateSemaphore()
时所指定的 lMaximumCount。
请记住, lpPreviousCount 所传回来的是一个瞬间值。你不可以把lReleaseCount 加上 *lpPreviousCount,就当作是 semaphore 的现值,因为其他线程可能已经改变了 semaphore 的值。
与 mutex 不同的是,调用 ReleaseSemaphore()
的那个线程,并不一定就得是调用 Wait...() 的那个线程。任何线程都可以在任何时间调用ReleaseSemaphore(),解除被任何线程锁定的 semaphore。
为什么 semaphore 要有一个初值
CreateSemaphore() 的第二个参数是 lInitialCount , 它的存在理由和CreateMutex() 的 bInitialOwner 参数的存在理由是一样的。如果你把初值设定为 0,你的线程就可以在产生 semaphore 之后进行所有必要的初始化工作。待初始化工作完成后,调用
ReleaseSemaphore() 就可以把现值增加到其最大可能值。
以环状缓冲区(ring buffer)为例,semaphore 通常被产生时是以 0 为初值,所以任何一个等待中的线程都会停下来。一旦有东西被加到环状缓冲区中,我们就以 ReleaseSemaphore() 增加 semaphore 的值,于是等待中的线程就可以继续进行。
如果“将数据写入环状缓冲区”的那个线程,在它(或任何其他线程)调用 Wait...() 函数之前,先调用 ReleaseSemaphore(),会出现想象不到的结果。就某种意义而言,这就完全退缩到 mutex 的运作情况了。