实现信号量
在本课程中,你将学习如何设计和实现一个简单的信号量类(Semaphore)。 信号量是一种用于控制对共享资源访问的同步机制。它通过一个计数器来跟踪可用资源的数量,并提供两个基本操作:`wait`(等待)和`signal`(信号)。当一个线程调用 `wait` 时,如果资源可用,则获取一个资源并减少计数器;否则,该线程将被阻塞,直到有资源可用。当一个线程完成使用资源后,调用 `signal` 来释放资源并增加计数器。 在 Ruby 中,你可以使用内置的 `Mutex` 和 `ConditionVariable` 类来实现信号量。以下是实现一个简单信号量类的基本步骤: 1. 定义一个类 `Semaphore`,并初始化一个计数器(表示可用资源数量)。 2. 实现 `wait` 方法:如果计数器大于 0,则减少计数器并返回;否则,线程进入等待状态,直到其他线程调用 `signal`。 3. 实现 `signal` 方法:增加计数器,并唤醒一个等待的线程。 下面是一个简单的实现示例: ```ruby class Semaphore def initialize(count) @count = count @mutex = Mutex.new @condition = ConditionVariable.new end def wait @mutex.synchronize do while @count == 0 @condition.wait(@mutex) end @count -= 1 end end def signal @mutex.synchronize do @count += 1 @condition.broadcast end end end ``` 这个实现确保了多线程环境下的安全访问。`wait` 方法使用循环检查计数器是否为 0,以防止虚假唤醒。`signal` 方法通过广播通知所有等待的线程,可能有新的资源可用。 你可以用这个信号量来保护有限的资源,例如数据库连接池、文件句柄或网络连接等。通过限制同时访问资源的线程数量,可以避免资源耗尽或竞争条件。 总结:信号量是并发编程中的重要工具,能够有效管理共享资源的访问。通过 Ruby 的 `Mutex` 和 `ConditionVariable`,你可以轻松实现一个功能完整的信号量类。
实现信号量
Ruby 的 concurrent gem 确实提供了自己的 Semaphore 实现;然而,使用监视器(monitor)来学习实现一个信号量是一个有趣的练习。
简要来说,信号量是一种允许部分线程并行访问一组固定资源的机制。始终将信号量视为拥有固定数量的许可。一旦所有许可都被分配出去,请求许可的线程就必须等待,直到有许可被归还后才能继续执行。
你的任务是实现一个信号量,其构造函数接收允许的最大许可数量,并且初始化时也具有相同数量的许可。此外,如果所有许可都已分配完毕,信号量应阻塞尝试获取许可的线程。

解决方案
根据上述定义,我们可以开始思考我们的 Semaphore 类需要暴露哪些方法。我们需要一个“获取许可”的方法和一个“释放许可”的方法。
acquire()方法用于模拟获取许可release()方法用于模拟释放许可
构造函数接受一个整数参数,表示信号量可用的许可数量。内部我们需要维护一个计数器,用于跟踪已经分配出去的许可数量。
目前,我们的 Semaphore 类的框架如下所示。
1class CountingSemaphore 2 3 def initialize(maxPermits) 4 @maxPermits = maxPermits 5 @givenOut = 0 6 end 7 8 def acquire() 9 10 end 11 12 def release() 13 14 end 15 16end
需要注意的是,acquire() 和 release() 这两个方法可能由不同的线程同时调用,因此我们必须使用某种锁来保护它们内部的逻辑。
我们可以使用 Monitor 类来实现互斥以及线程之间的通信。
现在让我们填充 acquire() 方法的实现。当线程不允许获取信号量时是什么情况?当所有许可都已分配出去时。这意味着当 givenOut == maxPermits 时,我们需要等待一个许可变得可用。如果该条件不成立,我们只需递增 givenOut 来模拟分配一个许可。由于在没有许可可用时需要 等待,这提示我们可以使用监视器对象创建一个条件变量,并对条件变量调用 wait()。
acquire() 方法的实现如下所示。注意,我们在方法末尾还调用了 broadcast() 通知条件变量。稍后我们将讨论为什么选择广播而不是单次唤醒。
1 def acquire() 2 3 @monitor.enter() 4 5 while @givenOut == @maxPermits 6 @condVar.wait() 7 end 8 9 @givenOut += 1 10 @condVar.broadcast() 11 12 @monitor.exit() 13 end
实现 release() 方法只需要简单地将 givenOut 变量减一。但是,什么时候应该像在 acquire() 方法中那样阻塞线程?当 givenOut == 0 时,再减一就没有意义了,因此我们应该在此条件下阻塞等待。
这可能看起来有些反直觉:你可能会问,为什么有人会在调用 acquire() 之前就调用 release()?这种情况完全可能发生,因为信号量也可以用于线程间的通信。一个线程可以在另一个线程调用同一信号量的 acquire() 之前就调用 release()。信号量没有所有权的概念!因此不同线程可以自由调用 acquire() 或 release() 方法。
这也意味着,每当我们在 givenOut 变量上进行增减操作时,都需要对条件变量调用 broadcast(),以确保其他方法中等待的线程能够继续执行。完整的实现如下:
1class CountingSemaphore 2 3 def initialize(maxPermits) 4 @maxPermits = maxPermits 5 @givenOut = 0 6 @monitor = Monitor.new 7 @condVar = @monitor.new_cond() 8 end 9 10 def acquire() 11 @monitor.enter() 12 13 while @givenOut == @maxPermits 14 @condVar.wait() 15 end 16 17 @givenOut += 1 18 @condVar.broadcast() 19 20 @monitor.exit() 21 end 22 23 def release() 24 @monitor.enter() 25 26 while @givenOut == 0 27 @condVar.wait() 28 end 29 30 @givenOut -= 1 31 @condVar.broadcast() 32 33 @monitor.exit() 34 end 35 36end
拓展问题 #1
注意,在两个方法中,我们都在对 givenOut 变量进行增减操作之后才调用 broadcast()。如果我们交换这两个语句的顺序,是否会有影响?答案是没有。因为在任一方法中的线程会一直持有与监视器关联的锁,直到调用 monitor.exit(),而这是两个方法的最后一条语句。等待条件变量的线程在被唤醒后,必须重新获取监视器才能继续执行,因此这两个语句的顺序并不重要。
拓展问题 #2
使用 broadcast() 还是 signal() 是否有区别?这样思考:是否可能有两个线程同时在 acquire() 和 release() 方法中因条件变量而阻塞?假设确实如此,那么 signal() 可能唤醒了一个条件仍为假的错误线程。该线程将再次进入等待状态,程序就会陷入死锁,因为 signal() 只唤醒一个线程。然而,如果等待条件变量的线程只能出现在与发出信号的方法不同的方法中,那么使用 broadcast() 还是 signal() 就无关紧要了。使用 signal() 仅允许一个等待线程继续执行,其余线程将继续等待。
测试用例
在下面的例子中,我们有两个线程:一个线程总是获取信号量,另一个线程总是释放信号量。两个线程的执行是交错进行的。我们使用信号量作为两个线程之间的通信机制。
1class CountingSemaphore
2
3 def initialize(maxPermits)
4 @maxPermits = maxPermits
5 @givenOut = 0
6 @monitor = Monitor.new
7 @condVar = @monitor.new_cond()
8 end
9
10 def acquire()
11 @monitor.enter()
12
13 while @givenOut == @maxPermits
14 @condVar.wait()
15 end
16
17 @givenOut += 1
18 @condVar.broadcast()
19
20 @monitor.exit()
21 end
22
23 def release()
24 @monitor.enter()
25
26 while @givenOut == 0
27 @condVar.wait()
28 end
29
30 @givenOut -= 1
31 @condVar.broadcast()
32
33 @monitor.exit()
34 end
35
36end
37
38semaphore = CountingSemaphore.new(1)
39
40t1 = Thread.new do
41
42 sleep(2)
43 puts "releasing"
44 semaphore.release()
45
46 sleep(2)
47 puts "releasing"
48 semaphore.release()
49
50 sleep(2)
51 puts "releasing"
52 semaphore.release()
53
54end
55
56t2 = Thread.new do
57
58 semaphore.acquire()
59
60 puts "acquiring"
61 semaphore.acquire()
62
63 puts "acquiring"
64 semaphore.acquire()
65
66 puts "acquiring"
67 semaphore.acquire()
68
69
70end
71
72t1.join()
73t2.join()