|
| 1 | +# 单例模式中选择 threading.Lock 的设计决策 |
| 2 | + |
| 3 | +## 🤔 问题背景 |
| 4 | + |
| 5 | +在实现 FastAPI Keystone 的单例模式时,我们面临一个重要的技术选择:使用 `threading.Lock` 还是 `asyncio.Lock` 来确保线程安全? |
| 6 | + |
| 7 | +## 🎯 设计目标 |
| 8 | + |
| 9 | +1. **通用兼容性**:支持同步和异步环境 |
| 10 | +2. **简单易用**:不增加不必要的复杂性 |
| 11 | +3. **高性能**:适合单例创建的使用场景 |
| 12 | +4. **可靠性**:经过实战验证的技术方案 |
| 13 | + |
| 14 | +## ⚖️ 技术对比 |
| 15 | + |
| 16 | +### threading.Lock vs asyncio.Lock |
| 17 | + |
| 18 | +| 特性 | threading.Lock | asyncio.Lock | |
| 19 | +|------|---------------|--------------| |
| 20 | +| 使用环境 | 同步 + 异步 | 仅异步 | |
| 21 | +| 语法要求 | 简单 `with` | 需要 `async with` | |
| 22 | +| 依赖关系 | 标准库,无额外依赖 | 需要事件循环 | |
| 23 | +| 性能开销 | 低 | 稍高(异步调度) | |
| 24 | +| 学习成本 | 低 | 中等 | |
| 25 | +| 适用场景 | 通用 | 异步密集型 | |
| 26 | + |
| 27 | +## 🔍 具体原因分析 |
| 28 | + |
| 29 | +### 1. **兼容性需求** |
| 30 | + |
| 31 | +AppInjector 需要在多种场景中使用: |
| 32 | + |
| 33 | +```python |
| 34 | +# 场景1: 应用启动(同步) |
| 35 | +def create_app(): |
| 36 | + app = FastAPI() |
| 37 | + injector = AppInjector([DatabaseModule()]) # 必须支持同步 |
| 38 | + return app |
| 39 | + |
| 40 | +# 场景2: 依赖注入(可能同步或异步) |
| 41 | +def get_injector() -> AppInjector: |
| 42 | + return AppInjector([AppModule()]) # 需要兼容性 |
| 43 | + |
| 44 | +# 场景3: 异步请求处理 |
| 45 | +async def api_handler(injector: AppInjector = Depends(get_injector)): |
| 46 | + service = injector.get_instance(UserService) # 实例获取通常是同步的 |
| 47 | + return await service.get_users() |
| 48 | +``` |
| 49 | + |
| 50 | +**如果使用 asyncio.Lock 的问题:** |
| 51 | +```python |
| 52 | +# ❌ 这样会导致语法错误 |
| 53 | +def create_app(): |
| 54 | + injector = await AppInjector([DatabaseModule()]) # SyntaxError! 同步函数不能用await |
| 55 | + |
| 56 | +# ❌ 强制使用异步会让API变得复杂 |
| 57 | +async def get_injector() -> AppInjector: |
| 58 | + return await AppInjector([AppModule()]) # 不必要的异步化 |
| 59 | +``` |
| 60 | + |
| 61 | +### 2. **使用场景特征** |
| 62 | + |
| 63 | +单例模式的典型使用特点: |
| 64 | + |
| 65 | +- **低频操作**:通常在应用启动时创建一次 |
| 66 | +- **短暂锁定**:锁持有时间极短(仅实例创建期间) |
| 67 | +- **非IO密集**:不涉及文件、网络等异步IO操作 |
| 68 | +- **混合环境**:需要在同步和异步代码中都能使用 |
| 69 | + |
| 70 | +这些特征使得 `threading.Lock` 更合适: |
| 71 | + |
| 72 | +```python |
| 73 | +# threading.Lock 适合的场景 |
| 74 | +with self._lock: # 快速获取锁 |
| 75 | + if cls not in instances: |
| 76 | + instances[cls] = cls(*args, **kwargs) # 快速创建实例 |
| 77 | +# 立即释放锁,不阻塞其他操作 |
| 78 | +``` |
| 79 | + |
| 80 | +### 3. **性能考虑** |
| 81 | + |
| 82 | +```python |
| 83 | +# 性能测试结果(从示例中) |
| 84 | +""" |
| 85 | +单线程创建100个实例用时: 0.0000秒 |
| 86 | +5线程并发创建500个实例用时: 0.0004秒 |
| 87 | +所有实例都相同: True |
| 88 | +""" |
| 89 | +``` |
| 90 | + |
| 91 | +`threading.Lock` 在单例场景中表现出色: |
| 92 | +- **微秒级锁定**:创建实例的时间极短 |
| 93 | +- **低开销**:不需要异步调度器 |
| 94 | +- **高并发**:支持多线程并发访问 |
| 95 | + |
| 96 | +### 4. **代码简洁性** |
| 97 | + |
| 98 | +```python |
| 99 | +# threading.Lock 版本 - 简洁直观 |
| 100 | +def singleton(cls): |
| 101 | + instances = {} |
| 102 | + lock = threading.Lock() |
| 103 | + |
| 104 | + def get_instance(*args, **kwargs): |
| 105 | + if cls not in instances: |
| 106 | + with lock: |
| 107 | + if cls not in instances: |
| 108 | + instances[cls] = cls(*args, **kwargs) |
| 109 | + return instances[cls] |
| 110 | + |
| 111 | + return get_instance |
| 112 | + |
| 113 | +# asyncio.Lock 版本 - 复杂且限制多 |
| 114 | +def async_singleton(cls): |
| 115 | + instances = {} |
| 116 | + lock = asyncio.Lock() |
| 117 | + |
| 118 | + async def get_instance(*args, **kwargs): # 必须是异步函数 |
| 119 | + if cls not in instances: |
| 120 | + async with lock: # 必须用 async with |
| 121 | + if cls not in instances: |
| 122 | + instances[cls] = cls(*args, **kwargs) |
| 123 | + return instances[cls] |
| 124 | + |
| 125 | + return get_instance # 返回的是异步函数,使用时必须 await |
| 126 | +``` |
| 127 | + |
| 128 | +## 🏗️ 架构优势 |
| 129 | + |
| 130 | +### 统一的编程模型 |
| 131 | +```python |
| 132 | +# 使用 threading.Lock,API 保持一致 |
| 133 | +injector = AppInjector([Module()]) # 同步和异步环境都可以这样用 |
| 134 | +service = injector.get_instance(Service) # 实例获取也是同步的 |
| 135 | + |
| 136 | +# 如果使用 asyncio.Lock,API 会分裂 |
| 137 | +injector = await AppInjector.create([Module()]) # 异步创建 |
| 138 | +service = await injector.get_instance(Service) # 异步获取,增加复杂性 |
| 139 | +``` |
| 140 | + |
| 141 | +### 更好的错误处理 |
| 142 | +```python |
| 143 | +# threading.Lock - 错误处理简单 |
| 144 | +try: |
| 145 | + injector = AppInjector([Module()]) |
| 146 | +except Exception as e: |
| 147 | + logger.error(f"Failed to create injector: {e}") |
| 148 | + |
| 149 | +# asyncio.Lock - 错误处理复杂 |
| 150 | +try: |
| 151 | + injector = await AppInjector.create([Module()]) |
| 152 | +except Exception as e: |
| 153 | + logger.error(f"Failed to create injector: {e}") |
| 154 | +# 还需要处理异步上下文错误 |
| 155 | +``` |
| 156 | + |
| 157 | +## 🔬 技术深度分析 |
| 158 | + |
| 159 | +### 并发模型差异 |
| 160 | + |
| 161 | +**threading.Lock**: |
| 162 | +- 基于操作系统线程 |
| 163 | +- 抢占式调度 |
| 164 | +- 适合 CPU 密集和混合场景 |
| 165 | +- 与 FastAPI 的线程池集成良好 |
| 166 | + |
| 167 | +**asyncio.Lock**: |
| 168 | +- 基于协程调度 |
| 169 | +- 协作式调度 |
| 170 | +- 适合 IO 密集场景 |
| 171 | +- 需要整个调用链都是异步的 |
| 172 | + |
| 173 | +### 内存和性能开销 |
| 174 | + |
| 175 | +```python |
| 176 | +# threading.Lock 开销分析 |
| 177 | +threading.Lock() # 系统级锁,开销极小 |
| 178 | +with lock: # 直接系统调用,快速 |
| 179 | + pass # 操作完成,立即释放 |
| 180 | + |
| 181 | +# asyncio.Lock 开销分析 |
| 182 | +asyncio.Lock() # 需要事件循环支持 |
| 183 | +async with lock: # 需要异步调度器介入 |
| 184 | + await ... # 可能涉及协程切换 |
| 185 | +``` |
| 186 | + |
| 187 | +## 📊 实际测试数据 |
| 188 | + |
| 189 | +基于 `examples/advanced/lock-comparison/main.py` 的测试结果: |
| 190 | + |
| 191 | +| 测试场景 | threading.Lock | asyncio.Lock | |
| 192 | +|---------|---------------|--------------| |
| 193 | +| 同步环境使用 | ✅ 完美支持 | ❌ 无法使用 | |
| 194 | +| 异步环境使用 | ✅ 完美支持 | ✅ 支持 | |
| 195 | +| 混合环境 | ✅ 无缝切换 | ❌ 需要API分离 | |
| 196 | +| 100次创建耗时 | ~0.0000秒 | ~0.0001秒 | |
| 197 | +| 500次并发耗时 | ~0.0004秒 | 不适用 | |
| 198 | + |
| 199 | +## 🎯 最终决策 |
| 200 | + |
| 201 | +**选择 `threading.Lock` 的原因总结:** |
| 202 | + |
| 203 | +1. **✅ 通用兼容性**:同步异步环境都可用 |
| 204 | +2. **✅ API 简洁性**:不需要 await 语法 |
| 205 | +3. **✅ 性能优秀**:微秒级锁定,低开销 |
| 206 | +4. **✅ 学习成本低**:使用简单,易于理解 |
| 207 | +5. **✅ 生态兼容**:与 FastAPI 生态完美集成 |
| 208 | +6. **✅ 维护简单**:代码路径少,bug 风险低 |
| 209 | + |
| 210 | +**asyncio.Lock 的局限性:** |
| 211 | + |
| 212 | +1. **❌ 环境限制**:只能在异步上下文使用 |
| 213 | +2. **❌ API 复杂**:强制异步化简单操作 |
| 214 | +3. **❌ 性能开销**:异步调度器介入 |
| 215 | +4. **❌ 学习门槛**:需要深度理解异步编程 |
| 216 | +5. **❌ 生态割裂**:与同步代码无法兼容 |
| 217 | + |
| 218 | +## 💡 设计哲学 |
| 219 | + |
| 220 | +> **"选择最适合的工具,而不是最新的工具"** |
| 221 | +
|
| 222 | +在单例模式中,我们的目标是: |
| 223 | +- 确保唯一性(✅ 两种锁都能做到) |
| 224 | +- 线程安全(✅ 两种锁都能做到) |
| 225 | +- 使用简单(✅ threading.Lock 更胜一筹) |
| 226 | +- 性能优秀(✅ threading.Lock 更适合) |
| 227 | +- 通用兼容(✅ threading.Lock 独有优势) |
| 228 | + |
| 229 | +因此,`threading.Lock` 是单例模式的最佳选择。 |
| 230 | + |
| 231 | +## 🔄 何时考虑 asyncio.Lock |
| 232 | + |
| 233 | +如果你的使用场景满足以下条件,可以考虑 `asyncio.Lock`: |
| 234 | + |
| 235 | +1. **纯异步环境**:整个应用都是异步的 |
| 236 | +2. **IO 密集操作**:单例创建涉及异步 IO |
| 237 | +3. **长时间锁定**:需要在锁内执行异步操作 |
| 238 | +4. **协程优化**:需要避免阻塞事件循环 |
| 239 | + |
| 240 | +```python |
| 241 | +# 适合 asyncio.Lock 的场景示例 |
| 242 | +class AsyncDatabaseSingleton: |
| 243 | + async def __new__(cls): |
| 244 | + async with cls._lock: |
| 245 | + if cls._instance is None: |
| 246 | + cls._instance = super().__new__(cls) |
| 247 | + await cls._instance._async_init() # 异步初始化 |
| 248 | + return cls._instance |
| 249 | + |
| 250 | + async def _async_init(self): |
| 251 | + # 建立异步数据库连接 |
| 252 | + self.connection = await asyncpg.connect(DATABASE_URL) |
| 253 | +``` |
| 254 | + |
| 255 | +但对于 FastAPI Keystone 的 AppInjector,这些条件都不满足,所以 `threading.Lock` 是正确选择。 |
| 256 | + |
| 257 | +## 📚 参考资料 |
| 258 | + |
| 259 | +- [Python threading.Lock 文档](https://docs.python.org/3/library/threading.html#lock-objects) |
| 260 | +- [Python asyncio.Lock 文档](https://docs.python.org/3/library/asyncio-sync.html#asyncio.Lock) |
| 261 | +- [FastAPI 并发模型](https://fastapi.tiangolo.com/async/) |
| 262 | +- [Single-threaded vs Multi-threaded in Python](https://realpython.com/python-concurrency/) |
0 commit comments