摘要: 本文针对「a.py 常驻运行、定时刷新数据,多个 Python 文件需要读取最新值」这一常见需求,先讲清进程内存不共享的本质,再按「同进程 → 跨进程」递进给出四种可直接复制的完整方案:同进程 import 加守护线程刷新、独立进程加 JSON 文件原子写、独立进程加 Redis、独立进程加 HTTP 服务。每种方案都配有目录结构、完整代码、启动方式和优缺点分析,最后提供四种方案的对比选型表、七个高频踩坑清单和可直接套用的项目模板,帮助读者根据是否同进程、数据规模、实时性和跨机器需求快速做出选择。
适用场景:
a.py常驻运行、定时刷新数据,多个 py 文件需要读取最新的值。本文给出 4 种可运行的完整方案,按「同进程 → 跨进程」逐步递进,直接复制即可用。
目录
-
- 先搞清楚问题本质
-
方案一:同进程 import + 守护线程刷新
-
方案二:独立进程 + JSON 文件原子写
-
方案三:独立进程 + Redis
-
方案四:独立进程 + HTTP 服务
-
四种方案对比与选型
-
踩坑清单
0. 先搞清楚问题本质
Python 的两个进程,内存是不共享的。 这句话决定了你只能用哪类方案。
| 关系 | 能否直接读变量 | 说明 |
|---|---|---|
同一进程,import 模块 |
✅ 能 | 用 模块名.变量 访问,实时 |
两个独立的 python xxx.py 进程 |
❌ 不能 | 必须走文件 / Redis / HTTP / socket |
「a.py 定时更新」意味着它大概率是常驻进程 ,所以如果其他脚本是单独 python b.py 跑的,方案一无效,只能走方案二/三/四。
⚠️ 特别注意:python a.py 之后,另一个文件里 import a,不会 读到那个进程的内存,而是把 a.py 的代码又执行了一遍,得到的是另一个全新进程里的全新变量。这是新手最大的误解。
方案一:同进程 import + 守护线程刷新
适用场景
所有读取者都从同一个入口(比如 main.py)启动,a.py 只是被 import 的模块,不单独 python a.py。这是最简单、最快、零依赖的方案。
目录结构
text
project/
├── a.py # 数据源模块(后台线程定时刷新)
├── b.py # 读取者 1
└── c.py # 读取者 2
a.py 完整代码
python
# a.py
import threading
import time
import random
---------- 内部状态 ----------
_lock = threading.Lock() # 读写锁
_data = {} # 当前数据快照
_ready = threading.Event() # 首次加载完成的信号
def _fetch():
"""模拟:从数据库 / 接口 / 串口 取数。替换成你自己的逻辑。"""
return {
"ts": time.time(),
"symbol": "AAPL",
"value": random.randint(1, 100),
}
def _refresh_loop(interval=5):
"""后台线程:每 interval 秒刷新一次"""
while True:
try:
new = _fetch()
with _lock: # 写锁
_data.clear()
_data.update(new) # 原地更新,保持引用
_ready.set() # 通知 get_data 可以读了
except Exception as e:
print(f"[a] 刷新失败: {e}")
time.sleep(interval)
模块被 import 时立即启动一次,且只启动一次(sys.modules 缓存保证)
_thread = threading.Thread(
target=_refresh_loop,
kwargs={"interval": 5},
daemon=True, # 主程序退出时自动结束
name="a-refresher",
)
_thread.start()
def get_data(wait=True, timeout=10):
"""对外唯一接口:返回最新数据的副本"""
if wait:
_ready.wait(timeout) # 可选:等首次加载完成
with _lock:
return dict(_data) # 返回副本,防止外部改到内部
b.py 完整代码(读取者)
python
# b.py
import time
import a # 触发 a.py 顶层代码 → 启动刷新线程
for i in range(3):
print(f"[b] 第{i+1}次:", a.get_data())
time.sleep(6) # 6 > 5,能看到数据变化
c.py 完整代码(另一个读取者)
python
# c.py
import time
import a
while True:
d = a.get_data()
if d:
print(f"[c] value={d['value']} ts={d['ts']:.1f}")
time.sleep(5)
启动方式
bash
python b.py # 单独跑可以
python c.py # 单独跑也可以(每个进程各自有一份 a 的线程)
如果你想让 b、c 共享同一份 a 的数据,必须让它们在同一个进程里跑,例如:
python
# main.py
import threading
import a, b, c
t1 = threading.Thread(target=b.run, daemon=True)
t2 = threading.Thread(target=c.run, daemon=True)
t1.start(); t2.start()
t1.join(); t2.join()
(b.py / c.py 需要把逻辑包进 def run())
逐行解释
| 代码 | 作用 |
|---|---|
threading.Lock() |
防止读到「写了一半」的字典 |
_data.clear(); _data.update(new) |
原地更新,外部如果持有 _data 引用也能看到变化 |
daemon=True |
主线程退出时不被这个死循环卡住 |
| 模块顶层启动线程 | import a 只执行一次,保证线程唯一 |
get_data() 返回 dict(_data) |
防止调用方改了返回值影响内部状态 |
优点 / 缺点
-
✅ 零依赖、纳秒级访问、无序列化开销
-
❌ 只适合同进程,跨进程无效
-
❌ 若
a.py被python a.py直接跑,其他进程 import 到的是另一份内存
方案二:独立进程 + JSON 文件原子写
适用场景
a.py 是独立常驻进程(python a.py),其他脚本也是独立进程,同一台机器,数据不大(几 MB 以内),延迟几十毫秒可接受。
目录结构
text
project/
├── shared_data.json # 数据文件(自动生成)
├── a.py # 生产者
├── reader1.py # 消费者 1
└── reader2.py # 消费者 2
a.py 完整代码(生产者)
python
# a.py
import json
import os
import tempfile
import time
import random
DATA_FILE = os.path.join(os.path.dirname(os.path.abspath(file)), "shared_data.json")
def write_atomic(path, obj):
"""
原子写:先写同目录临时文件,再 os.replace 替换。
os.replace 在同一文件系统内是原子操作,读端永远看到完整文件。
"""
d = os.path.dirname(path)
fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
try:
with os.fdopen(fd, "w", encoding="utf-8") as f:
json.dump(obj, f, ensure_ascii=False)
os.replace(tmp, path) # ★ 原子替换
except BaseException:
try:
os.unlink(tmp) # 异常时清理临时文件
except FileNotFoundError:
pass
raise
def fetch():
"""模拟取数,替换成你的业务逻辑"""
return {
"ts": time.time(),
"symbol": "AAPL",
"value": random.randint(1, 100),
}
if name == "main":
print(f"[a] 开始写数据 → {DATA_FILE}")
while True:
try:
data = fetch()
write_atomic(DATA_FILE, data)
print(f"[a] {time.strftime('%H:%M:%S')} 写入 value={data['value']}")
except Exception as e:
print(f"[a] 写入失败: {e}")
time.sleep(5)
reader1.py 完整代码(消费者)
python
# reader1.py
import json
import os
import time
DATA_FILE = os.path.join(os.path.dirname(os.path.abspath(file)), "shared_data.json")
def read_shared(max_age=None): """ 读共享文件。 max_age: 数据超过多少秒算过期,None 表示不检查 """ try: with open(DATA_FILE, encoding="utf-8") as f: data = json.load(f) except FileNotFoundError: return None # a.py 还没写第一次 except json.JSONDecodeError: return None # 极端情况兜底(理论上原子写不会发生)
if max_age is not None and time.time() - data.get("ts", 0) > max_age:
print("[reader1] 数据过期了")
return None
return data
while True: d = read_shared(max_age=30) if d: age = time.time() - d"ts" print(f"reader1 value={d'value'} 数据年龄={age:.1f}s") else: print("reader1 暂无数据") time.sleep(2)
reader2.py 完整代码(另一个消费者,逻辑相同)
python
# reader2.py
import json, os, time
DATA_FILE = os.path.join(os.path.dirname(os.path.abspath(file)), "shared_data.json")
while True:
try:
with open(DATA_FILE, encoding="utf-8") as f:
d = json.load(f)
print(f"[reader2] {d['symbol']} = {d['value']}")
except FileNotFoundError:
print("[reader2] 等 a.py 首次写入...")
time.sleep(3)
启动方式(开三个终端)
bash
python a.py # 终端1:常驻,每 5 秒写一次
python reader1.py # 终端2
python reader2.py # 终端3
关键解释
为什么必须「原子写」?
如果你这样写:
python
with open("data.json", "w") as f:
json.dump(data, f) # ❌ 危险
在 json.dump 执行到一半时,读端去 json.load,会读到截断的 JSON ,直接 JSONDecodeError。数据越大越容易触发。
正确做法三步:
-
在同目录创建临时文件(
tempfile.mkstemp(dir=d)) -
写入完整内容
-
os.replace(tmp, path)------ 这一步在 POSIX / Windows 上都是原子的
读端要么读到旧文件,要么读到新文件,永远不会读到半个文件。
为什么要同目录?
os.replace 跨文件系统不是原子的(会退化成复制)。所以临时文件必须和目标文件在同一目录。
读取延迟
写端每 5 秒写一次,读端每 2 秒读一次,最坏延迟 ≈ 7 秒。如果要更快,就缩短两边的间隔,但会占用更多 CPU / IO。
优点 / 缺点
-
✅ 零依赖、单文件、可人肉排查(直接
cat shared_data.json) -
✅ 进程崩溃不影响历史数据
-
❌ 有轮询延迟
-
❌ 数据大了(> 几十 MB)序列化开销明显 → 换
pickle或msgpack -
❌ 只适合同机,不能跨机器
如果要存 DataFrame / numpy
把 json.dump 换成 pickle.dump,读端 pickle.load,原子写逻辑完全一样:
python
import pickle
def write_atomic_pkl(path, obj):
d = os.path.dirname(path)
fd, tmp = tempfile.mkstemp(dir=d, suffix=".tmp")
with os.fdopen(fd, "wb") as f:
pickle.dump(obj, f, protocol=pickle.HIGHEST_PROTOCOL)
os.replace(tmp, path)
方案三:独立进程 + Redis
适用场景
-
多个消费进程 / 多台机器都要读
-
需要实时性(毫秒级)
-
需要「最新值」语义(永远读到最后一次写的)
前置准备
bash
pip install redis
# 本地装 Redis:
# Docker: docker run -d -p 6379:6379 redis
# Ubuntu: sudo apt install redis-server
# Mac: brew install redis && brew services start redis
目录结构
text
project/
├── a.py # 生产者:定时 SET
├── reader1.py # 消费者 1
└── reader2.py # 消费者 2
a.py 完整代码(生产者)
python
# a.py
import json
import time
import random
import redis
r = redis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)
KEY = "app:data"
def fetch():
return {
"ts": time.time(),
"symbol": "AAPL",
"value": random.randint(1, 100),
}
if name == "main":
while True:
data = fetch()
# SET 本身是原子的,读端要么读到旧值,要么读到新值
r.set(KEY, json.dumps(data, ensure_ascii=False))
# 可选:设个过期时间,防止 a.py 挂了以后读到「僵尸数据」
# r.expire(KEY, 60)
print(f"[a] 写入 {data}")
time.sleep(5)
reader1.py 完整代码(消费者)
python
# reader1.py
import json
import time
import redis
r = redis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)
KEY = "app:data"
while True:
raw = r.get(KEY)
if raw is None:
print("[reader1] 暂无数据")
else:
d = json.loads(raw)
age = time.time() - d["ts"]
print(f"[reader1] value={d['value']} 年龄={age:.1f}s")
time.sleep(2)
reader2.py 完整代码(用 pub/sub 实时推送,不是轮询)
如果你希望「a.py 一更新,reader 立刻收到」,用 Redis Pub/Sub:
python
# a.py(发布者,追加在 set 之后)
r.publish("app:data:channel", json.dumps(data, ensure_ascii=False))
python
# reader2.py(订阅者)
import json
import redis
r = redis.Redis(host="127.0.0.1", port=6379, db=0, decode_responses=True)
p = r.pubsub()
p.subscribe("app:data:channel")
print("[reader2] 已订阅,等待推送...")
for msg in p.listen():
if msg["type"] != "message":
continue
d = json.loads(msg["data"])
print(f"[reader2] 收到推送: {d}")
Pub/Sub vs GET:
GET是「拉」模式,读端主动查,有轮询延迟,但能读到最新值(即使刚启动)
Pub/Sub是「推」模式,零延迟,但离线期间的消息会丢,刚启动时读不到历史值生产上常见做法:启动时先 GET 一次拿当前值,再 SUB 接收增量
启动方式
bash
# 先确保 Redis 在跑
redis-cli ping # 返回 PONG 就 OK
python a.py
python reader1.py
python reader2.py
关键解释
| 点 | 说明 |
|---|---|
decode_responses=True |
自动把 bytes 解码成 str,省得手动 .decode() |
r.set() 原子性 |
Redis 单命令原子,不会有「读到半截 JSON」的问题 |
r.expire(KEY, 60) |
防止 a.py 挂了以后消费者读到很久以前的旧值。加上后,读端拿到 None 就知道数据源挂了 |
| Pub/Sub 无持久化 | 订阅者掉线期间的消息会丢,不适合「必须拿到每一条」的场景(那要用 Redis Stream / Kafka) |
优点 / 缺点
-
✅ 实时、原子、可跨机器、多消费者天然支持
-
✅ 支持过期、数据结构丰富
-
❌ 需要额外部署 Redis 服务
-
❌ 引入了外部依赖(挂了就全挂)
方案四:独立进程 + HTTP 服务
适用场景
-
跨机器 / 跨语言(前端、Go、Java 都能调)
-
需要鉴权、限流、监控
-
想把 a.py 变成一个「数据服务」
前置准备
bash
pip install flask requests
目录结构
text
project/
├── a.py # 数据服务:Flask 提供 GET /data
└── reader.py # 客户端:requests 请求
a.py 完整代码(Flask 服务 + 后台刷新线程)
python
# a.py
import threading
import time
import random
from flask import Flask, jsonify
app = Flask(name)
---------- 后台刷新的数据 ----------
_lock = threading.Lock() _data = {}
def _fetch(): return { "ts": time.time(), "symbol": "AAPL", "value": random.randint(1, 100), }
def _refresh_loop(interval=5): while True: try: new = _fetch() with _lock: _data.clear() _data.update(new) except Exception as e: print(f"a 刷新失败: {e}") time.sleep(interval)
---------- HTTP 接口 ----------
@app.route("/data", methods="GET") def get_data(): with _lock: return jsonify(dict(_data))
@app.route("/health", methods="GET") def health(): return {"status": "ok"}
if name == "main": # 后台刷新线程(daemon,Flask 退出时自动结束) threading.Thread(target=_refresh_loop, kwargs={"interval": 5}, daemon=True, name="refresher").start()
# 开发用 app.run;生产用 gunicorn / uvicorn
app.run(host="0.0.0.0", port=8000, threaded=True)</pre>
reader.py 完整代码(HTTP 客户端)
python
# reader.py
import time
import requests
URL = "http://127.0.0.1:8000/data"
while True:
try:
resp = requests.get(URL, timeout=2)
resp.raise_for_status()
d = resp.json()
age = time.time() - d["ts"]
print(f"[reader] value={d['value']} 年龄={age:.1f}s")
except requests.RequestException as e:
print(f"[reader] 请求失败: {e}")
time.sleep(2)
启动方式
bash
python a.py # 启动服务,监听 8000
python reader.py # 另一个终端
用 curl 也能测
bash
curl http://127.0.0.1:8000/data
# {"symbol":"AAPL","ts":1729900000.123,"value":42}
生产部署(不要用 app.run)
app.run() 是开发服务器,单进程、无并发保护。生产环境用 gunicorn:
bash
pip install gunicorn
注意:gunicorn 多 worker 时,每个 worker 会各自跑一个刷新线程 → 数据不一致!
解决:把刷新线程放到单独进程,或改用 --workers 1
gunicorn -w 1 -b 0.0.0.0:8000 a:app
⚠️ 重要坑 :
gunicorn -w 4会启动 4 个 worker 进程,a.py的顶层刷新线程会跑 4 份,_data也各有各的。此时应该:
用
-w 1(单 worker,多线程处理请求),或把「刷新 + 存储」独立成单独进程,Flask 只做读取(数据从 Redis / 文件读)
优点 / 缺点
-
✅ 语言无关、跨机器、天然支持鉴权 / 限流 / 监控
-
✅ 客户端极简单(
requests.get) -
❌ 需要跑一个 Web 服务,部署稍重
-
❌ 有网络开销(本机 ~1ms,跨机器看网络)
-
❌ gunicorn 多 worker 时需要额外处理数据一致性
四种方案对比与选型
| 维度 | 方案一 import | 方案二 JSON 文件 | 方案三 Redis | 方案四 HTTP |
|---|---|---|---|---|
| 进程关系 | 必须同进程 | 跨进程、同机 | 跨进程、可跨机 | 跨进程、可跨机 |
| 依赖 | 无 | 无 | Redis 服务 | Flask / gunicorn |
| 延迟 | 纳秒 | 秒级(轮询) | 毫秒 | 毫秒~几十毫秒 |
| 实时性 | 最高 | 低 | 高 | 高 |
| 多消费者 | 天然支持 | 天然支持 | 天然支持 | 天然支持 |
| 跨语言 | ❌ | 半(读 JSON) | ✅ | ✅ |
| 可调试性 | 差 | 好(看文件) | 中(redis-cli) | 好(curl) |
| 代码复杂度 | 低 | 低 | 中 | 中 |
一句话选型
text
a.py 和其他 py 在同一个进程里?
├─ 是 → 【方案一】import + 守护线程(最简单)
└─ 否 → 数据要共享给谁?
├─ 只有本机几个 py,数据不大 → 【方案二】JSON 原子写
├─ 本机/多机,要实时 → 【方案三】Redis
└─ 跨语言 / 对外提供服务 → 【方案四】HTTP
踩坑清单
坑 1:from a import data 拿到的是快照
python
from a import data # ❌ 快照,之后 a.data 变了这里不会变
print(data)
import a # ✅ 用属性访问
print(a.data)
原理:from ... import ... 是在导入那一刻把对象绑定 到当前模块的全局命名空间,之后 a.data = ... 重新赋值不会同步。
坑 2:python a.py 之后别的进程 import a 拿不到数据
bash
# 终端1
python a.py # 进程 A,内存里有 data
终端2
python b.py # import a → 又执行一遍 a.py → 进程 B,全新内存
两个进程内存独立,方案一在跨进程场景下完全无效。必须走方案二/三/四。
坑 3:写文件不用原子替换,读端随机报错
python
open("data.json", "w").write(json.dumps(d)) # ❌ 读端可能读到半截
必须:
python
tmp = "data.json.tmp"
with open(tmp, "w") as f:
f.write(json.dumps(d))
os.replace(tmp, "data.json") # ✅ 原子
坑 4:daemon=True 的线程会随着主线程退出被杀掉
python
t = threading.Thread(target=loop, daemon=True)
主程序 sys.exit() 时,daemon 线程不会执行 finally / 清理逻辑。如果刷新线程里有资源要释放(比如关数据库连接),用 daemon=False + 主线程里主动 join,或者注册 atexit。
坑 5:Redis 里存了数据但 a.py 挂了,读端一直拿到旧值
用 expire 兜底:
python
r.set(KEY, json.dumps(data))
r.expire(KEY, 60) # 60 秒不刷新就自动删除
读端拿到 None 就知道数据源挂了,而不是拿到三天前的数据还以为是真的。
坑 6:gunicorn 多 worker 导致刷新线程跑多份
bash
gunicorn -w 4 a:app # ❌ 4 个进程各自跑一份刷新线程,_data 各不一致
gunicorn -w 1 a:app # ✅ 单进程多线程
或者把刷新逻辑放到独立进程,Web 服务只做读取(从 Redis / 文件读)。
坑 7:pickle / json 每次都全量序列化,数据大时 CPU 爆炸
数据量 > 10 MB 时考虑:
-
msgpack(比 JSON 小、快) -
pyarrow/parquet(列式,适合 DataFrame) -
multiprocessing.shared_memory(同一台机器、零拷贝)
完整项目模板
如果你想直接拿来用,推荐结构:
project/
├── a.py # 数据生产者(常驻)
├── readers/
│ ├── reader1.py
│ └── reader2.py
├── shared_data.json # 方案二产物
└── requirements.txt
requirements.txt(按需选择):
# 方案二
# (无依赖)
方案三
redis>=5.0
方案四
flask>=3.0
requests>=2.31
gunicorn>=21.0 # 生产部署
一句话总结 :同进程用方案一,本机小数据用方案二,实时多机用方案三,对外服务用方案四。判断标准永远是「是不是同一个进程」,这一条决定了 90% 的选型。