信号量(Semaphore)是一种计数器,用于控制能同时访问某个资源的线程数量。它主要有两个操作:P(等待)和 V(释放)。当一个线程要访问共享资源时,它会调用 P 操作,如果计数器大于 0,则将计数器的值减 1,并允许访问资源;如果计数器等于 0,则线程被阻塞,直到计数器大于 0。当一个线程访问完共享资源后,会调用 V 操作,将计数器的值加 1,以允许其他线程访问资源。
在 Python 的 threading.Semaphore
类和 threading.BoundedSemaphore
类中,acquire()
方法对应 P 操作,release()
方法对应 V 操作。
Python 的 threading.Semaphore
类的官方文档:https://docs.python.org/zh-cn/3/library/threading.html?highlight=threading#threading.Semaphore
Python 的 threading.BoundedSemaphore
类的官方文档:https://docs.python.org/zh-cn/3/library/threading.html?highlight=threading#threading.BoundedSemaphore
相较于 Semaphore
类,BoundedSemaphore
类不允许信号量的值超过初始值,即无论当调用 release()
方法的次数比调用 acquire()
方法多时,会抛出 ValueError
异常。
样例 1:使用信号量控制并行线程数
我们定义一个 use_semaphore()
函数,模拟函数执行的操作;通过加入 0.1 * idx
的延时时候,模拟不同线程运行时间不同。然后连续启动 3 个线程,模拟 3 个线程并发抢占信号量的情况。
from threading import Thread, Semaphore
import time
def use_semaphore(idx: int, semaphore: Semaphore) -> None:
"""获取信号量,持有 1 + 0.1 * idx 秒后释放的方法
Parameters
----------
idx : int
线程编号
semaphore : Semaphore
Semaphore 信号量或 BoundedSemaphore 信号量
"""
semaphore.acquire()
print(f"[{time.time()}] thread: {idx} acquire semaphore\n", end='')
time.sleep(1 + 0.1 * idx)
semaphore.release()
print(f"[{time.time()}] thread: {idx} release semaphore\n", end='')
def test():
"""多线程使用信号量的实验"""
semaphore = Semaphore(2) # 即最多允许 2 个线程同时运行
thread_lst = []
for i in range(1, 4):
thread = Thread(target=use_semaphore, args=(i, semaphore))
thread.start()
thread_lst.append(thread)
for thread in thread_lst:
thread.join()
if __name__ == "__main__":
test()
运行结果:
[1697001264.923071] thread: 1 acquire semaphore
[1697001264.923071] thread: 2 acquire semaphore
[1697001266.0269325] thread: 1 release semaphore
[1697001266.0269325] thread: 3 acquire semaphore
[1697001266.136337] thread: 2 release semaphore
[1697001267.3356693] thread: 3 release semaphore
因为 Semaphore 实例化时的参数为 2,即最多允许 2 个线程同时运行。当第 1 个线程和第 2 个线程 acquire
了信号量后,此时信号量的值为 0,第 3 个线程就进入了等待,直到第 1 个线程释放 release
信号量后,第 3 个线程才结束 acquire
状态开始运行。
样例 2:BoundedSemaphore 防止异常调用 release()
的作用
现在,在样例 1 的基础上,我们假设开发者不小心在 use_semaphore()
函数多的 semaphore.acquire()
前写入了一个 semaphore.release()
。
def use_semaphore(idx: int, semaphore: Semaphore) -> None:
"""获取信号量,持有 1 + 0.1 * idx 秒后释放的方法
Parameters
----------
idx : int
线程编号
semaphore : Semaphore
Semaphore 信号量或 BoundedSemaphore 信号量
"""
semaphore.release() # 误写多出了这一行
semaphore.acquire()
print(f"[{time.time()}] thread: {idx} acquire semaphore\n", end='')
time.sleep(1 + 0.1 * idx)
semaphore.release()
print(f"[{time.time()}] thread: {idx} release semaphore\n", end='')
此时,运行结果中可以看到,3 个线程被并行运行了。
[1697002008.9587426] thread: 1 acquire semaphore
[1697002008.9587426] thread: 2 acquire semaphore
[1697002008.9598155] thread: 3 acquire semaphore
[1697002010.0612736] thread: 1 release semaphore
[1697002010.1685946] thread: 2 release semaphore
[1697002010.26117] thread: 3 release semaphore
显然,这样的情况并不是我们期望的。BoundedSemaphore
类不允许信号量的值超过初始值的作用就体现出来了,通过使用 BoundedSemaphore
类,可以抛出异常,方便我们发现逻辑漏洞。
from threading import Thread, Semaphore, BoundedSemaphore
import time
def use_semaphore(idx: int, semaphore: Semaphore) -> None:
"""获取信号量,持有 1 + 0.1 * idx 秒后释放的方法
Parameters
----------
idx : int
线程编号
semaphore : Semaphore
Semaphore 信号量或 BoundedSemaphore 信号量
"""
semaphore.release() # 误写多出了这一行
semaphore.acquire()
print(f"[{time.time()}] thread: {idx} acquire semaphore\n", end='')
time.sleep(1 + 0.1 * idx)
semaphore.release()
print(f"[{time.time()}] thread: {idx} release semaphore\n", end='')
def test():
"""多线程使用信号量的实验"""
semaphore = BoundedSemaphore(2)
thread_lst = []
for i in range(1, 4):
thread = Thread(target=use_semaphore, args=(i, semaphore))
thread.start()
thread_lst.append(thread)
for thread in thread_lst:
thread.join()
if __name__ == "__main__":
test()
在运行结果中,会抛出异常:
ValueError: : Semaphore released too many timesSemaphore released too many times