Python http.server 共享状态时如何避免线程请求互相覆盖
你在用Python内置的http.server开发简单Web服务时,如果有跨请求共享状态的需求,直接操作全局可变变量很容易碰到多线程并发冲突,不同请求的数据互相覆盖的问题,只要给共享资源加线程互斥锁做访问限制,或者把单请求独立的属性绑定到当前处理线程的上下文里,就能避免这类异常。
所有多线程都会读写的共享可变状态,必须加互斥锁做同步保护,仅当前请求用到的私有数据不要存到全局域,直接挂到当前请求处理实例的属性下,就能彻底杜绝不同请求线程互相覆盖数据的问题。
用 ThreadingHTTPServer 做本地接口时,最容易忽略的不是端口,而是 handler 线程之间共享了同一个可变对象。两个请求都先读到旧计数,再分别写回新值,就会出现请求明明处理了 100 次,结果却只有 63 次的情况。解决办法是:把共享状态放到 server 对象上,用 threading.Lock 保护最小的读改写区;响应发送、文件读取和其他慢操作放在锁外。
ThreadingHTTPServer的并发 handler 不应直接无锁修改共享字典或列表。- 锁保护的是一次完整的读改写,不只是赋值那一行。
- 先在锁内生成不可变快照,再在锁外组装和发送响应。
先把共享状态的边界画出来
BaseHTTPRequestHandler 实例适合保存当前请求的数据;如果计数要跨请求保留,就应放在自定义 server 实例上。这样做的关键不是“所有代码都加锁”,而是明确谁拥有状态、哪段代码会同时读写。

from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
import json
import threading
class AppServer(ThreadingHTTPServer):
# server 对象保存跨请求状态;锁与状态必须属于同一个生命周期
def __init__(self, address, handler):
super().__init__(address, handler)
self.state = {"count": 0, "recent": []}
self.state_lock = threading.Lock()
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
# 一次完整的读改写必须在同一把锁内完成,避免丢失更新
with self.server.state_lock:
self.server.state["count"] += 1
self.server.state["recent"] = (self.server.state["recent"] + [self.path])[-10:]
snapshot = {
"count": self.server.state["count"],
"recent": list(self.server.state["recent"]),
}
# 锁外只使用快照,避免网络写入拖住其他请求
body = json.dumps(snapshot, ensure_ascii=False).encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "application/json; charset=utf-8")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
这里的锁覆盖了“读取旧值、修改、生成快照”三个动作。只锁住 += 1 仍然可能让最近请求列表和计数来自不同时间点;反过来,把 wfile.write() 放进锁里又会让慢客户端阻塞所有请求。
基线指标要能复现,而不是凭感觉判断
可以把初始计数清零,连续发固定数量的并发请求,再比较最终值和预期值。下面的客户端只负责制造压力,文章中的数值是说明并发覆盖问题的示例,不代表某台机器的固定性能。
from concurrent.futures import ThreadPoolExecutor
from urllib.request import urlopen
def hit(_):
# 每个任务访问同一个接口,响应内容只用于确认请求完成
with urlopen("http://127.0.0.1:8000/hit", timeout=2) as response:
return response.status
with ThreadPoolExecutor(max_workers=16) as pool:
statuses = list(pool.map(hit, range(100)))
# 只统计完成的请求,最终 count 应与成功请求数一致
print(sum(status == 200 for status in statuses))
| 检查项 | 无锁实现 | 锁保护读改写 |
|---|---|---|
| 计数结果 | 可能小于完成请求数 | 应与成功请求数一致 |
| 响应生成 | 容易读到半更新状态 | 使用锁外快照 |
| 锁占用范围 | 没有边界 | 只覆盖内存状态变更 |

锁不是所有共享数据的终点
这套方案只适合单进程内的短小状态。若请求要排队处理,用 queue.Queue 比让多个线程抢同一个列表更直观;若数据要在多个进程或多台机器间共享,应使用数据库、Redis 等外部存储。ThreadingHTTPServer 也更适合开发工具和小型内部服务,正式生产服务要额外考虑超时、连接关闭、日志和进程模型。
还有一个常见坑:不要把锁定义在 do_GET() 内部,那样每次请求都会得到新锁,线程之间根本无法互斥。锁必须与共享状态拥有相同的生命周期。
相关问题
为什么有了 GIL 还会丢计数?
GIL 不等于业务级读改写原子性。线程可能在读取旧值后切换,另一个线程完成同样的读取,最后两个结果互相覆盖。
可以用 RLock 替代 Lock 吗?
可以,但只有同一线程需要嵌套获取同一把锁时才有必要。普通的共享计数优先使用 Lock,边界更简单。
为什么要复制 recent 列表?
复制后响应使用的是稳定快照,锁释放后其他线程继续更新原列表,也不会改变当前响应准备发送的数据。
Go select default 分支导致消费者空转时如何降载
- 上一篇
- Go select default 分支导致消费者空转时如何降载
- 下一篇
- Go context.AfterFunc 停止失败时如何判断回调是否已开始
-
- 文章 · python教程 | 2小时前 |
- Python urllib.parse.quote_plus 处理空格和加号有什么区别
- 453浏览 收藏
-
- 文章 · python教程 | 3小时前 |
- Python json.loads 解析超大整数时如何限制输入风险
- 212浏览 收藏
-
- 文章 · python教程 | 4小时前 |
- Python zoneinfo 处理夏令时重复小时要看 fold 吗
- 102浏览 收藏
-
- 文章 · python教程 | 5小时前 |
- Python Fraction 从浮点数构造为什么得到很长分数
- 411浏览 收藏
-
- 文章 · python教程 | 7小时前 | python · decimal · 数值计算 · context Python Decimal quantize localcontext
- Python decimal 局部精度和全局上下文如何隔离
- 346浏览 收藏
-
- 文章 · python教程 | 1天前 | csv · python · csv DictReader restkey restval
- csv DictReader 缺列怎么配置或排查
- 434浏览 收藏
-
- 文章 · python教程 | 1天前 | 包管理 · python · 排错 · Python版本 pyproject.toml importlib.metadata
- importlib.metadata 版本怎么配置或排查
- 287浏览 收藏
-
- 文章 · python教程 | 1天前 | 命令行 · 编码 · python · subprocess · encoding subprocess TEXT stdout stderr
- subprocess 文本输出怎么配置或排查
- 444浏览 收藏
-
- 文章 · python教程 | 1天前 | python · logging · QueueListener · QueueHandler ·
- logging QueueHandler怎么配置或排查
- 303浏览 收藏
-
- 文章 · python教程 | 1天前 |
- sqlite3 autocommit怎么配置或排查
- 488浏览 收藏
-
- 文章 · python教程 | 1天前 |
- cache 与 lru_cache怎么配置或排查
- 377浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 23次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 126次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 51次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 21次使用
-
- OpenCompass
- OpenCompass是上海AI实验室推出的开源大模型评测平台,提供CompassKit、CompassHub和CompassRank三大核心组件,支持LLM及多模态模型的一站式标准化评估与排行榜查询。
- 74次使用
-
- Go map 并发写 panic 怎么办:从共享 map 到可控写入路径
- 2026-06-30 123浏览
-
- Go保证并发安全底层实现详解
- 2023-02-24 417浏览
-
- Go语言开发保证并发安全实例详解
- 2023-01-07 328浏览
-
- Golang 手写一个简单的并发任务 manager
- 2022-12-23 367浏览
-
- Go语言使用goroutine及通道实现并发详解
- 2023-01-02 221浏览

