fix(multifactor): make lazy calculation thread-safe - #495
Merged
fasiondog merged 9 commits intoAug 3, 2026
Merged
Conversation
- m_calculated: bool -> std::atomic<bool>, acquire/release DCLP - Extract clearCalculatedData(), called before build and on failure - calculate(): _checkData under try, failure cleans and rethrows - reset(): hold mutex for whole reset, _reset then clear then store false - getters: delegate to calculate() which has lock-free fast path - setters: relaxed store false (mutation not concurrent with reads) - Extract style regression QR to style_regression.cpp serial kernel - Remove process-global Eigen::setNbThreads during neutralization - wait_for_all_non_blocking: drain all futures before returning ABI: MultiFactorBase layout changes, all C++ extensions must recompile
- style_regression.cpp excluded from unity group to prevent Eigen header leakage into sibling sources - wait_for_all_non_blocking: yield first then bounded 1ms backoff
- drain: one task throws while another delayed task runs, exception returns only after all futures drained (spin-synchronized start) - concurrent first access: 32 threads mixed getters, build exactly once - failed first call: dirty derived state cleaned, original exception propagates, second call succeeds on clean state - reset recalculates without stale results - nested MF calculation triggers another MF lazily, no deadlock - serialization load does not publish stale state - style regression golden values, NaN row, rank deficient, Eigen thread configuration unchanged under concurrency
- style regression golden residuals corrected to OLS solution [0.4, -1.2, 1.2, -0.4] for y=[2,1,4,3], x=[0,1,2,3] - Eigen thread config check unconditional: nbThreads() always exists, assert unchanged after concurrent calls regardless of OpenMP - add clone independence test: ready original + clone accessed concurrently, only clone recalculates, results identical
test_style_regression.cpp includes Eigen/Core; the unit-test target was missing the eigen package, unlike the core target
MULTIFACTOR_IMP declares _calculate in the class body; defining it inline caused C2535 duplicate declaration. Move to out-of-class definitions matching the built-in subclass pattern.
unit-test links against hikyuu.dll; without HKU_API the symbol was not exported and the test binary failed to link (LNK2019)
woleigegg
marked this pull request as draft
August 3, 2026 09:54
Match the multifactor directory naming convention where compiled translation units use PascalCase (MultiFactorBase, NormalizeBase, ScoreRecord) rather than snake_case.
fasiondog
marked this pull request as ready for review
August 3, 2026 17:59
fasiondog
reviewed
Aug 3, 2026
Owner
There was a problem hiding this comment.
其实,这个wait_for_all_non_blocking 改动意义不大,这个就是微小的权衡影响,类似的很早就尝试过。后面就属于AI总在这没事微调了。
Owner
There was a problem hiding this comment.
应该说是,不同场景下的微调,平衡所有常用场景。本身影响不大,但AI在变得不同场景下使用时,总会喜欢在这里改来改去。
Contributor
Author
There was a problem hiding this comment.
嗯嗯,谢谢老师给的建议,我后续pr尽可能小一些,做好约束和审查。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
RANK插件修复系列PR,后续修复还在进行中。
问题概述
MultiFactorBase的惰性计算calculate()存在 C++ 数据竞争:m_calculated为普通bool,锁外读 + 锁内写且锁后不二次检查,多线程首次访问同一未计算实例时可能重复重建内部容器,导致持有 getter 内部引用的线程撕裂读或悬空引用。同时,计算失败时异常被吞掉仍设m_calculated = true,向下游发布半成品。此外,风格因子中性化路径中进程级
Eigen::setNbThreads全局切换在并发 MF 实例间互相污染,wait_for_all_non_blocking在子任务抛异常时提前返回会遗留未执行任务。根因分析
缺陷 1:
calculate()check-then-lock 无 double-checkbool锁外读与锁内写构成 C++ data race (UB)。m_stk_factor_by_date时若有线程通过getAllScores()返回的引用正在读取,造成撕裂读/悬空引用。缺陷 2:失败时发布半成品
_checkData()或构建过程抛异常时,异常被吞掉,m_calculated仍设为true,下游 getter 返回空的/半成品容器,调用方无法感知失败。缺陷 3:
Eigen::setNbThreads进程级全局竞态两个 MF 实例并发
calculate()时,一方setNbThreads(16)恢复全局值,另一方仍在回归中——进程级全局状态被互相污染。缺陷 4:
wait_for_all_non_blocking提前返回遗留任务一个子任务抛异常、另一个 delayed 子任务尚未启动时
run_available_task_once()返回 false → 直接return,遗留未执行任务。calculate()的 catch 块进入清理时后台仍有任务在跑,可能访问已释放资源。修复方案
1.
m_calculated改std::atomic<bool>+ acquire/release DCLP2. 提取
clearCalculatedData()清除所有计算后派生数据 (
m_ref_dates/m_stk_map/m_all_factors/m_date_index/m_stk_factor_by_date/m_ic),保留配置成员。calculate()构建前和异常后均调用,确保重试基于干净状态。3. getters 统一委托
calculate()所有 getter (
getDatetimeList/getFactor/getAllFactors/getScores/getAllScores/getIC) 改为直接调calculate(),由其 lock-free 快路径承担已就绪检查。getIC()的m_mutex锁在calculate()之后获取,严格顺序不嵌套,无自死锁。4. setters 用
relaxed store falsesetQuery/setRefStock/setStockList/setRefFactorSet/setNormalize/addSpecialNormalize/paramChanged改为m_calculated.store(false, relaxed)。setter 不与 getter 并发执行(见已知局限),只需让下次calculate()知道该重算。5.
reset()全程持锁6. 提取
StyleRegression.cpp串行内核将
calculate_residuals从MultiFactorBase.cpp静态函数提取为StyleRegression.cpp的calculate_style_residuals,删除进程级Eigen::setNbThreads调用。回归矩阵均为栈局部对象,外层按日并行天然可重入,不依赖全局线程配置。7.
wait_for_all_non_blockingdrain 修正删除
run_available_task_once()返回 false 时的提前return,改为继续等待所有 future 就绪,确保调用方get()抛异常时无遗留任务。验证
编译
build ok, spent 319.5s,0 error(2 个既有 C4018 warning,非本 PR 引入)功能验证
PR1 新增 13 个测试用例,逐个单独运行确认通过:
test_MF_thread_safe_concurrent_first_accesstest_MF_failed_first_call_clean_retrytest_MF_reset_recalculates_cleantest_MF_nested_calculate_no_deadlocktest_MF_clone_independent_statetest_MF_serialization_load_recalculatestest_style_regression_perfect_fittest_style_regression_golden_values[0.4, -1.2, 1.2, -0.4]test_style_regression_insufficient_samplestest_style_regression_nan_rowtest_style_regression_rank_deficienttest_style_regression_eigen_threads_unchangedEigen::nbThreads()不变(防回归)test_global_wait_drains_all_futures_on_exception回归测试
完整 unit-test 套件(排除 benchmark):
0 回归。
改动范围
hikyuu_cpp/hikyuu/trade_sys/multifactor/MultiFactorBase.hm_calculated→atomic<bool>,clearCalculatedData()声明,并发语义注释,序列化 load 改 atomic storehikyuu_cpp/hikyuu/trade_sys/multifactor/MultiFactorBase.cpphikyuu_cpp/hikyuu/trade_sys/multifactor/StyleRegression.hcalculate_style_residuals声明(HKU_API导出)hikyuu_cpp/hikyuu/trade_sys/multifactor/StyleRegression.cpphikyuu_cpp/hikyuu/utilities/thread/algorithm.hwait_for_all_non_blockingdrain 修正hikyuu_cpp/hikyuu/xmake.luaStyleRegression.cpp排除 unity build 组hikyuu_cpp/unit_test/hikyuu/trade_sys/multifactor/test_MF_ThreadSafe.cpphikyuu_cpp/unit_test/hikyuu/trade_sys/multifactor/test_style_regression.cpphikyuu_cpp/unit_test/hikyuu/utilities/thread/test_algorithm.cpphikyuu_cpp/unit_test/xmake.lua总计 10 文件,+883/-147 行。
性能影响
m_calculated.load(acquire)为 lock-free 原子读,与旧版if (m_calculated)普通读相比可忽略。relaxed原子读,可忽略。setNbThreads调用,回归矩阵为栈局部对象不依赖全局配置。已知局限与后续工作
1. 并发边界:不支持 getter 与配置 mutation 并发
现状:本 PR 实现多线程同时首次触发
calculate/getter 的安全性及计算完成后的并发只读。头文件注释 (MultiFactorBase.h:175-184) 明确划界。局限:不支持 getter 与
reset/setQuery/setStockList/setRefFactorSet/setParam同时执行,不支持调用方持有 getter 返回的内部引用期间另一线程修改实例。getter 返回内部容器引用,若要支持读写完全并发需改为不可变快照或复制返回,属更大 API 重构。后续:独立 issue 评估 getter 返回值改为
shared_ptr<const ...>的 API 影响。2. Python 子类
_reset()自死锁风险现状:
reset()改为全程持锁并调用虚函数_reset()。所有内置派生类 (EqualWeight/IC/ICIR/Weight) 均未覆盖_reset(),用基类空实现,当前无死锁。头文件注释 (MultiFactorBase.cpp:179) 已声明约束。局限:
MultiFactorBase通过 pybind trampoline 导出,Python 子类可覆盖_reset()。若 Python_reset()内调用self.calculate()/getIC()(均需m_mutex),会自死锁于非递归std::mutex。文档约束已声明,但语言层面未阻止。后续:可选方案(独立 issue)—
reset()拆分为锁外_resetUnsafe()+ 锁内清理,或改用std::recursive_mutex。3. DCLP 非 single-flight,高并发首次 miss 锁争用
现状:采用 acquire/release DCLP + 锁后二次检查。首个拿到锁的线程构建,后续线程通过二次检查跳过重算。
局限:在首个构建线程释放锁前,其他等待锁的线程排队进入慢路径,存在锁串行化延迟。
后续:独立 PR 引入 Pending/Ready 分离 +
promise/futuresingle-flight,需满足嵌套 RANK 和线程池兼容验收标准后方可合并。4. ThreadSanitizer 未验证
现状:新增 13 个并发测试用例覆盖 PLAN §17.1 验收矩阵的 4/5 项。
局限:开发环境为 Windows MSVC,无 ThreadSanitizer 支持,未做动态竞态检测。正确性依靠内存序推理(release/acquire happens-before)和代码审查。
后续:在 Linux + clang/gcc + TSan 环境补跑 unit-test,可作为独立 CI 步骤。
5. ABI 变更
现状:
m_calculated从bool改为std::atomic<bool>,改变MultiFactorBase对象布局。局限:新 hikyuu core + 旧
extind.dll(未经重编)= 不受支持配置。仓库内所有消费者一起重编,无 in-repo 破坏。风险限于 out-of-tree C++ 插件按旧头文件编译而链接新库。后续:插件包应声明最低核心 build/version,发布说明中明确二进制兼容性要求。
6. selector 异常传播行为变化
现状:旧
calculate()捕获异常后仍设m_calculated = true,下游 selector 在空数据上静默运行。新实现清理半成品、保持 false、原异常传播。MultiFactorSelector/MultiFactorSelector2的_calculate()和_getSelected()中四处调用m_mf->calculate()/getScores()无 try/catch。局限:这不是局限,是有意的行为修正——旧实现让
_checkData()失败被吞掉,selector 选出 0 只股票而调用方看不到异常。新实现让异常传播至上层框架捕获,符合"失败不应伪装成功"原则。Python 端由 pybind11 自动转换异常。后续:无需后续 issue。若上游反馈某些场景需容错,应在 selector 层加 try/catch + 降级策略,而非回退 MF 异常语义。
不在本 PR 范围的后续工作
RANK 完整修复按设计文档拆分为六步 PR。本 PR 仅含 PR1(核心 MF 并发修复)。确定性截面排序(tie 不确定)、进程级数据 revision、插件结构化缓存 key(碰撞/先发布后计算)、不可变 RankPanel、并发构建合并分别由后续 PR 独立交付,依赖 PR1 完成后推进。