python

关注公众号 jb51net

关闭
首页 > 脚本专栏 > python > Python有界信号量BoundedSemaphore

一文详解Python有界信号量BoundedSemaphore原理、用法及应用场景

作者:小灰灰搞电子

在多线程与多进程编程中,信号量是一种经典的同步原语,用于控制对共享资源的并发访问数量,本文基于 Python 最新稳定版本,深入讲解 BoundedSemaphore 的原理、用法、与普通 Semaphore 的区别,以及实际应用场景,需要的朋友可以参考下

一、Python BoundedSemaphore 有界信号量详解

1、 引言

在多线程与多进程编程中,信号量(Semaphore)是一种经典的同步原语,用于控制对共享资源的并发访问数量。Python 标准库 threading 模块提供了 Semaphore 与 BoundedSemaphore 两个类,其中 BoundedSemaphore 在 Semaphore 的基础上增加了「有界」约束,能有效防止因 release() 调用次数超过 acquire() 而导致的信号量计数无限增长。

本文基于 Python 最新稳定版本(3.12+,部分特性在 3.13 中进一步增强),深入讲解 BoundedSemaphore 的原理、用法、与普通 Semaphore 的区别,以及实际应用场景。

2、信号量基础回顾

2.1、 什么是信号量

信号量内部维护一个计数器:

import threading

sem = threading.Semaphore(2)  # 允许 2 个并发

def worker():
    sem.acquire()
    try:
        # 临界区
        pass
    finally:
        sem.release()

2.2、 普通 Semaphore 的问题

普通 Semaphore 允许 release() 被任意多次调用,即使没有对应的 acquire()。这会导致计数器不断增大,超出初始值,从而失去「限制并发数」的意义:

sem = threading.Semaphore(2)
sem.release()  # 计数变为 3
sem.release()  # 计数变为 4
# 并发上限被悄悄放大

3、 BoundedSemaphore 的原理

3.1、 有界约束

BoundedSemaphore 继承自 Semaphore,唯一区别在于:当 release() 导致计数器超过初始值时,会抛出 ValueError。

import threading

bounded = threading.BoundedSemaphore(2)
bounded.release()  # ValueError: Semaphore released too many times

这一机制能帮助开发者尽早发现代码中的错误——例如 release() 被重复调用、或 acquire() 失败后仍执行了 release()。

3.2、 源码实现

在 CPython 的 threading.py 中,BoundedSemaphore 的实现非常简洁:

class BoundedSemaphore(Semaphore):
    def __init__(self, value=1):
        super().__init__(value)
        self._initial_value = value

    def release(self, n=1):
        with self._cond:
            if self._value + n > self._initial_value:
                raise ValueError("Semaphore released too many times")
            return super().release(n)

注意:在 Python 3.13 中,Semaphore.release() 新增了 n 参数,支持一次释放多个许可;BoundedSemaphore 同步支持该参数,并在释放后校验是否越界。

4、基本用法

4.1、 标准「获取-释放」模式

import threading
import time

# 最多允许 3 个线程同时访问
bounded = threading.BoundedSemaphore(3)

def worker(name):
    bounded.acquire()
    try:
        print(f"{name} 进入临界区")
        time.sleep(1)
    finally:
        bounded.release()

threads = [threading.Thread(target=worker, args=(f"线程-{i}",)) for i in range(5)]
for t in threads:
    t.start()
for t in threads:
    t.join()

4.2、 使用上下文管理器

Semaphore 支持上下文管理器协议,可简化代码并确保异常时也能正确释放:

bounded = threading.BoundedSemaphore(2)

def worker(name):
    with bounded:
        print(f"{name} 正在执行")
        time.sleep(0.5)

4.3 、一次获取多个许可(3.13+)

bounded = threading.BoundedSemaphore(5)

# 一次获取 3 个许可
bounded.acquire(3)
try:
    # 需要 3 个资源的操作
    pass
finally:
    bounded.release(3)

5、BoundedSemaphore 与 Semaphore 对比

特性SemaphoreBoundedSemaphore
计数可超过初始值是否
多余 release 行为静默增加计数抛出 ValueError
适用场景需要动态扩展并发上限严格限制并发数、排查错误
性能开销略低几乎无差异(仅多一次比较)

选择建议:除非有特殊需求,否则优先使用 BoundedSemaphore,它能在开发阶段暴露 release() 滥用问题。

6、 实际应用场景

6.1、 数据库连接池

import threading
import time

class ConnectionPool:
    def __init__(self, size):
        self._sem = threading.BoundedSemaphore(size)
        self._connections = [self._create_conn() for _ in range(size)]

    def _create_conn(self):
        return object()  # 模拟数据库连接

    def get_conn(self):
        self._sem.acquire()
        try:
            return self._connections.pop()
        except IndexError:
            # 实际项目中连接会被归还,这里仅作演示
            self._sem.release()
            raise

    def return_conn(self, conn):
        self._connections.append(conn)
        self._sem.release()

6.2、 限流器

import threading
import time

class RateLimiter:
    def __init__(self, max_concurrent):
        self._sem = threading.BoundedSemaphore(max_concurrent)

    def __enter__(self):
        self._sem.acquire()

    def __exit__(self, exc_type, exc_val, exc_tb):
        self._sem.release()
        return False

# 使用
limiter = RateLimiter(5)
with limiter:
    # 最多 5 个并发请求
    pass

6.3、 生产者-消费者模型

import threading
import queue
import time

bounded = threading.BoundedSemaphore(3)  # 缓冲区最多 3 个

def producer(q):
    for i in range(10):
        bounded.acquire()
        q.put(i)
        print(f"生产: {i}")
        time.sleep(0.2)

def consumer(q):
    while True:
        item = q.get()
        if item is None:
            break
        print(f"消费: {item}")
        bounded.release()
        q.task_done()

q = queue.Queue()
t1 = threading.Thread(target=producer, args=(q,))
t2 = threading.Thread(target=consumer, args=(q,))
t1.start()
t2.start()
t1.join()
q.put(None)
t2.join()

7、常见陷阱与最佳实践

7.1、 忘记释放导致死锁

bounded = threading.BoundedSemaphore(1)

def bad_worker():
    bounded.acquire()
    # 如果这里抛出异常且未用 try/finally,信号量永远不会释放
    raise RuntimeError("出错")

解决:始终使用 with 语句或 try/finally。

7.2、 重复释放导致 ValueError

bounded = threading.BoundedSemaphore(1)
bounded.acquire()
bounded.release()
bounded.release()  # ValueError

这正是 BoundedSemaphore 的价值所在——普通 Semaphore 会静默接受,掩盖 bug。

7.3 、与 asyncio 的区分

threading.BoundedSemaphore 是线程级同步原语,会阻塞线程;在异步代码中应使用 asyncio.BoundedSemaphore:

import asyncio

async def main():
    sem = asyncio.BoundedSemaphore(3)
    async with sem:
        await asyncio.sleep(1)

8、性能考量

BoundedSemaphore 基于 threading.Condition 实现,在竞争激烈时存在一定的上下文切换开销。对于极高并发的场景,可考虑:

9、 总结

BoundedSemaphore 是 Python 中一个简单而强大的同步工具:

在实际项目中,建议默认使用 BoundedSemaphore 而非 Semaphore,让并发边界更加清晰可控。

二、代码示例

import threading
import time

def demo_basic_limit():
    print("=" * 70)
    print("【场景1:BoundedSemaphore 并发限流,最大允许2个线程同时执行】")
    # 初始上限 value=2
    bsem = threading.BoundedSemaphore(value=2)

    def worker(tid):
        with bsem:
            print(f"线程 {tid} 获取到信号量,开始工作")
            time.sleep(1.0)
            print(f"线程 {tid} 释放信号量,结束工作")

    threads = [threading.Thread(target=worker, args=(i,)) for i in range(4)]
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    print("场景1执行完毕\n")


def demo_illegal_release():
    print("=" * 70)
    print("【场景2:核心特性:未acquire直接release,抛出ValueError(普通Semaphore不会报错)】")
    bsem = threading.BoundedSemaphore(2)
    # 正常获取释放
    bsem.acquire()
    bsem.release()
    print("✅ 正常 acquire → release 成功")

    try:
        # 没有上锁,直接release,计数超过初始值
        bsem.release()
        bsem.release()
        bsem.release()
    except ValueError as e:
        print(f"❌ 捕获异常 ValueError: {e}")
    print("场景2执行完毕\n")


def demo_non_blocking_and_timeout():
    print("=" * 70)
    print("【场景3:acquire 非阻塞 / 超时等待】")
    bsem = threading.BoundedSemaphore(1)
    # 主线程先占用
    bsem.acquire()

    def try_get(tid):
        # 非阻塞获取
        ok1 = bsem.acquire(blocking=False)
        print(f"线程{tid} 非阻塞获取结果: {ok1}")
        # 超时等待0.5秒
        ok2 = bsem.acquire(timeout=0.5)
        print(f"线程{tid} 等待0.5s获取结果: {ok2}")

    t = threading.Thread(target=try_get, args=(1,))
    t.start()
    t.join()
    bsem.release()
    print("场景3执行完毕\n")


def demo_manual_try_finally():
    print("=" * 70)
    print("【场景4:手动 acquire + try‑finally 安全释放(不使用with)】")
    bsem = threading.BoundedSemaphore(1)

    def task():
        acquired = False
        try:
            acquired = bsem.acquire(timeout=1)
            if acquired:
                print("手动拿到信号量,执行业务逻辑")
                time.sleep(0.3)
            else:
                print("获取信号量超时")
        finally:
            if acquired:
                bsem.release()
                print("finally 中安全释放信号量")

    t = threading.Thread(target=task)
    t.start()
    t.join()
    print("场景4执行完毕\n")


def compare_sem_vs_bounded():
    print("=" * 70)
    print("【场景5:对比普通Semaphore和BoundedSemaphore区别】")
    import queue
    sem = threading.Semaphore(1)
    bsem = threading.BoundedSemaphore(1)

    print("普通Semaphore可以无限release:")
    sem.release()
    sem.release()
    print("Semaphore多次release不会报错")

    print("\nBoundedSemaphore禁止超额release:")
    try:
        bsem.release()
    except ValueError as err:
        print(f"BoundedSemaphore报错:{err}")
    print("场景5执行完毕\n")


if __name__ == "__main__":
    demo_basic_limit()
    demo_illegal_release()
    demo_non_blocking_and_timeout()
    demo_manual_try_finally()
    compare_sem_vs_bounded()

D:\user\01417804\桌面\PythonProject\.venv\Scripts\python.exe D:\user\01417804\桌面\PythonProject\main.py 
======================================================================
【场景1:BoundedSemaphore 并发限流,最大允许2个线程同时执行】
线程 0 获取到信号量,开始工作
线程 1 获取到信号量,开始工作
线程 1 释放信号量,结束工作
线程 2 获取到信号量,开始工作
线程 0 释放信号量,结束工作
线程 3 获取到信号量,开始工作
线程 3 释放信号量,结束工作线程 2 释放信号量,结束工作

场景1执行完毕

======================================================================
【场景2:核心特性:未acquire直接release,抛出ValueError(普通Semaphore不会报错)】
✅ 正常 acquire → release 成功
❌ 捕获异常 ValueError: Semaphore released too many times
场景2执行完毕

======================================================================
【场景3:acquire 非阻塞 / 超时等待】
线程1 非阻塞获取结果: False
线程1 等待0.5s获取结果: False
场景3执行完毕

======================================================================
【场景4:手动 acquire + try‑finally 安全释放(不使用with)】
手动拿到信号量,执行业务逻辑
finally 中安全释放信号量
场景4执行完毕

======================================================================
【场景5:对比普通Semaphore和BoundedSemaphore区别】
普通Semaphore可以无限release:
Semaphore多次release不会报错

BoundedSemaphore禁止超额release:
BoundedSemaphore报错:Semaphore released too many times
场景5执行完毕


进程已结束,退出代码为 0

以上就是一文详解Python有界信号量BoundedSemaphore原理、用法及应用场景的详细内容,更多关于Python有界信号量BoundedSemaphore的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文