若需并发,请在应用内用队列/协程控制
多并发解决方案
给你两种简单好用的并发控制方案,避免开多 worker 占满显存、又能保证 SSE 正常流式输出。
方案 A(最快落地):信号量限流(限并发 N=1 或小于显卡承受上限)
- 特点:无需大改,直接在现有流式生成器外层加锁即可。
- 适合:先把系统稳定跑起来。
在 survey_service.py 顶部加:
python
import threading
GPU_CONCURRENCY = 1 # 根据显存调小,比如 1
gpu_sem = threading.BoundedSemaphore(GPU_CONCURRENCY)
在流式生成处包一层(以 outline_generation_api 为例,full_survey_generation_api 同理):
python
def outline_generation_api():
... 解析入参省略
def stream():
yield json.dumps({"stage": 0, "stageDsc": "queued"}) + "\n"
with gpu_sem: # 限制同时仅有 N 个任务占用模型/GPU
for chunk in generate_outline(
session_id, topic, model, section_num, outline_reference_num,
api_url, api_key, saving_path, req_discribe, language
):
yield chunk
return Response(stream_with_context(stream()), mimetype='text/event-stream')
启动时保持单进程:
bash
nohup uvicorn survey_service:asgi_app --host 0.0.0.0 --port 15001 --workers 1 --log-level=info >> logs/span_data_monitor.log 2>&1 &
方案 B(更平滑):应用内任务队列 + 后台工作线程(公平排队,前端可看到"排队中")
- 特点:请求快速返回流,真正的重活在后台工作线程里跑;可以控制并发为 1 或 2。
- 适合:需要"排队位次/保持连接"的场景。
在 survey_service.py 顶部加:
python
import threading, queue, time, json
GPU_CONCURRENCY = 1
gpu_sem = threading.BoundedSemaphore(GPU_CONCURRENCY)
task_queue = queue.Queue()
def worker_loop():
while True:
job = task_queue.get() # {"runner": callable->iterator, "out": Queue, "done": Event}
try:
with gpu_sem:
for chunk in job["runner"]():
job["out"].put(chunk)
except Exception as e:
job["out"].put(json.dumps({"stage": -1, "stageDsc": f"error: {e}"}) + "\n")
finally:
job["done"].set()
task_queue.task_done()
启动 N 个后台线程=N 即并发数
for _ in range(GPU_CONCURRENCY):
threading.Thread(target=worker_loop, daemon=True).start()
在接口里改为入队并从"专属输出队列"读(以 outline_generation_api 为例):
python
def outline_generation_api():
... 解析入参省略
out_q = queue.Queue(maxsize=256)
done = threading.Event()
def runner():
这里就是你原来的生成器
return generate_outline(
session_id, topic, model, section_num, outline_reference_num,
api_url, api_key, saving_path, req_discribe, language
)
入队
task_queue.put({"runner": runner, "out": out_q, "done": done})
position = task_queue.qsize() # 粗略队列位置(放入后得到的是当前长度)
def stream():
提示已排队
yield json.dumps({"stage": 0, "stageDsc": f"queued, pos≈{position}"}) + "\n"
持续把后台线程产出的分片写给客户端
while not done.is_set() or not out_q.empty():
try:
chunk = out_q.get(timeout=1)
yield chunk
except queue.Empty:
yield ":\n\n" # SSE keepalive
return Response(stream_with_context(stream()), mimetype='text/event-stream')
使用要点
- 仍然用单 worker 启动 uvicorn;并发控制交给应用内的信号量或队列。
- GPU_CONCURRENCY 根据显存设置(通常 1);需要提速时先评估模型占显存峰值再调大。
- 前端如需要"队列位置/预计等待"可在 stage:0 里携带 pos≈X,并定时轮询队列长度优化显示。
- 若任务很长,建议在生成器关键步骤(如每段生成、引用处理、docx 转换)追加阶段日志,方便观察进度。
两种方案可先用 A 快速稳定,再升级到 B 获得更好的排队体验。