问题描述
解决方案
在 QMT 量化平台中,如果您在策略中启动了额外的 Python 线程(例如用于执行耗时的计算、监控外部信号或异步IO操作),必须确保在策略停止时能够优雅地关闭这些线程,以防止资源泄露或后台残留进程。
QMT 提供了一个专门的生命周期函数 stop(ContextInfo) 来处理策略退出时的清理工作。
核心解决方案
- 使用
stop(ContextInfo)函数:这是 QMT 专门用于策略停止前执行清理工作的回调函数。 - 使用标志位控制线程循环:在
ContextInfo对象中设置一个布尔标志位(例如ContextInfo.is_running),线程内部循环检测该标志。 - 在
stop中修改标志并回收线程:当策略停止时,QMT 会自动调用stop函数。您需要在该函数中将标志位设为False,并调用thread.join()等待线程安全退出。
代码实现示例
以下是一个完整的策略代码示例,展示了如何在 init 中启动线程,并在 stop 中安全关闭它。
# -*- coding: gbk -*-
import threading
import time
def my_custom_task(ContextInfo):
"""
这是运行在子线程中的自定义任务函数
"""
print("子线程: 启动成功")
# 循环检查 ContextInfo.is_running 标志位
while ContextInfo.is_running:
# 执行您的后台任务逻辑
print(f"子线程: 正在运行... 当前时间: {time.strftime('%H:%M:%S')}")
# 模拟耗时操作,注意这里要有 sleep,避免死循环占用过多 CPU
time.sleep(2)
print("子线程: 检测到停止信号,正在退出...")
def init(ContextInfo):
"""
策略初始化函数
"""
print("策略初始化: init")
# 1. 设置线程运行控制标志位,挂载到 ContextInfo 上以便全局访问
ContextInfo.is_running = True
# 2. 创建线程,将 ContextInfo 作为参数传递进去
# 注意:daemon=False 表示非守护线程,我们需要手动管理它的生命周期
ContextInfo.worker_thread = threading.Thread(target=my_custom_task, args=(ContextInfo,))
# 3. 启动线程
ContextInfo.worker_thread.start()
print("策略初始化: 线程已启动")
def handlebar(ContextInfo):
"""
K线/Tick驱动函数
"""
# 这里是主线程逻辑
if ContextInfo.is_new_bar():
print(f"主线程: handlebar 运行中... BarPos: {ContextInfo.barpos}")
def stop(ContextInfo):
"""
策略停止函数:当用户点击停止或关闭策略时触发
"""
print("策略停止: stop 被调用")
# 1. 修改标志位,通知子线程退出循环
if hasattr(ContextInfo, 'is_running'):
ContextInfo.is_running = False
# 2. 等待子线程结束 (join)
# 设置 timeout 防止子线程卡死导致主程序无法退出
if hasattr(ContextInfo, 'worker_thread') and ContextInfo.worker_thread.is_alive():
print("策略停止: 等待子线程退出...")
ContextInfo.worker_thread.join(timeout=5)
print("策略停止: 子线程已关闭")
print("策略停止: 资源清理完毕")
关键点解析
-
ContextInfo的作用:
ContextInfo是贯穿整个策略生命周期的全局对象。我们将线程对象 (worker_thread) 和控制标志 (is_running) 都挂载在ContextInfo上,这样在init、handlebar和stop函数中都能访问到它们。 -
stop(ContextInfo)函数:
这是 QMT API 文档中明确规定的接口。当您在界面上点击“停止运行”或者关闭策略编辑器时,底层 C++ 框架会调用 Python 的stop函数。如果您的策略涉及文件句柄关闭、数据库连接断开或线程停止,必须写在这个函数里。 -
线程安全与阻塞:
- 在
stop函数中使用join()是为了确保子线程完全执行完当前的逻辑块并退出。 - 建议给
join(timeout=5)加上超时时间。如果子线程因为某些原因(如网络IO阻塞)无法响应停止信号,超时机制可以防止整个 QMT 客户端界面卡死。
- 在
-
避免在子线程中直接交易:
QMT 的交易函数(如passorder)通常不是线程安全的,且必须由主线程调用。建议子线程只负责计算或数据处理,将结果写入ContextInfo的某个变量,然后在handlebar(主线程)中检测该变量并执行下单操作。
Q&A
Q: 如果我不写 stop 函数会怎么样?
A: 如果不实现 stop 函数或不在其中关闭线程,当您在 QMT 界面停止策略时,Python 的主线程会结束,但您启动的子线程(如果是非守护线程)可能会继续在后台运行。这会导致日志持续输出、占用 CPU 资源,甚至在重新运行策略时导致逻辑冲突(例如多个线程同时写入同一个文件)。
Q: ContextInfo 对象在子线程中是线程安全的吗?
A: Python 的 GIL(全局解释器锁)在一定程度上保证了基本操作的原子性,但在并发读写复杂数据结构(如 List 或 Dict)时,建议使用 threading.Lock 进行保护。特别是当子线程写入数据,主线程 handlebar 读取数据时,加锁是最佳实践。
Q: 能否使用 thread.terminate() 强制杀死线程?
A: Python 标准库 threading 模块不支持强制终止线程。强制终止极易导致资源(如锁、文件句柄)无法释放,从而引发死锁或数据损坏。使用“标志位 + 循环检测”的软退出方式是唯一推荐的安全做法。