当前位置:首页 > 文章列表 > 文章 > python教程 > Python http.server 共享状态时如何避免线程请求互相覆盖

Python http.server 共享状态时如何避免线程请求互相覆盖

来源:17golang原创 2026-09-14 17:52:44 0浏览 收藏

你在用Python内置的http.server开发简单Web服务时,如果有跨请求共享状态的需求,直接操作全局可变变量很容易碰到多线程并发冲突,不同请求的数据互相覆盖的问题,只要给共享资源加线程互斥锁做访问限制,或者把单请求独立的属性绑定到当前处理线程的上下文里,就能避免这类异常。

所有多线程都会读写的共享可变状态,必须加互斥锁做同步保护,仅当前请求用到的私有数据不要存到全局域,直接挂到当前请求处理实例的属性下,就能彻底杜绝不同请求线程互相覆盖数据的问题。

ThreadingHTTPServer 做本地接口时,最容易忽略的不是端口,而是 handler 线程之间共享了同一个可变对象。两个请求都先读到旧计数,再分别写回新值,就会出现请求明明处理了 100 次,结果却只有 63 次的情况。解决办法是:把共享状态放到 server 对象上,用 threading.Lock 保护最小的读改写区;响应发送、文件读取和其他慢操作放在锁外。

要点速览
  • ThreadingHTTPServer 的并发 handler 不应直接无锁修改共享字典或列表。
  • 锁保护的是一次完整的读改写,不只是赋值那一行。
  • 先在锁内生成不可变快照,再在锁外组装和发送响应。

先把共享状态的边界画出来

BaseHTTPRequestHandler 实例适合保存当前请求的数据;如果计数要跨请求保留,就应放在自定义 server 实例上。这样做的关键不是“所有代码都加锁”,而是明确谁拥有状态、哪段代码会同时读写。

Python http.server 两个并发请求与 threading.Lock 保护共享 state 的结构示意图
图1:共享状态读改写的操作示意图;锁只包住内存状态更新,不包住响应发送。
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))
检查项无锁实现锁保护读改写
计数结果可能小于完成请求数应与成功请求数一致
响应生成容易读到半更新状态使用锁外快照
锁占用范围没有边界只覆盖内存状态变更
Python ThreadingHTTPServer 加锁后 count 快照与无锁加锁示例指标对比示意图
图2:加锁后的结果示意图;每次响应读取的是已经完成更新的稳定快照。

锁不是所有共享数据的终点

这套方案只适合单进程内的短小状态。若请求要排队处理,用 queue.Queue 比让多个线程抢同一个列表更直观;若数据要在多个进程或多台机器间共享,应使用数据库、Redis 等外部存储。ThreadingHTTPServer 也更适合开发工具和小型内部服务,正式生产服务要额外考虑超时、连接关闭、日志和进程模型。

还有一个常见坑:不要把锁定义在 do_GET() 内部,那样每次请求都会得到新锁,线程之间根本无法互斥。锁必须与共享状态拥有相同的生命周期。

相关问题

为什么有了 GIL 还会丢计数?

GIL 不等于业务级读改写原子性。线程可能在读取旧值后切换,另一个线程完成同样的读取,最后两个结果互相覆盖。

可以用 RLock 替代 Lock 吗?

可以,但只有同一线程需要嵌套获取同一把锁时才有必要。普通的共享计数优先使用 Lock,边界更简单。

为什么要复制 recent 列表?

复制后响应使用的是稳定快照,锁释放后其他线程继续更新原列表,也不会改变当前响应准备发送的数据。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go select default 分支导致消费者空转时如何降载Go select default 分支导致消费者空转时如何降载
上一篇
Go select default 分支导致消费者空转时如何降载
Go context.AfterFunc 停止失败时如何判断回调是否已开始
下一篇
Go context.AfterFunc 停止失败时如何判断回调是否已开始
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    23次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    126次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    51次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    21次使用
  • OpenCompass大模型评测体系详解:功能、使用指南与应用场景
    OpenCompass
    OpenCompass是上海AI实验室推出的开源大模型评测平台,提供CompassKit、CompassHub和CompassRank三大核心组件,支持LLM及多模态模型的一站式标准化评估与排行榜查询。
    74次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码