Common Lisp中lparallel与stmx兼容问题:如何让lparallel生成bordeaux-threads及解决方案
我最近尝试在lparallel环境中使用stmx库,执行以下代码时触发了异常:
(ql:quickload :stmx) (ql:quickload :lparallel) (setf lparallel:*kernel* (lparallel:make-kernel 4)) (lparallel:future (stmx:atomic (format t "~a ~%" "it works")))
报错信息如下:
Unhandled SIMPLE-ERROR in thread #<SB-THREAD:THREAD "lparallel" RUNNING {1003F8F073}>:
STMX internal error! stmx:current-thread contains a stale value: found #<SB-THREAD:THREAD "main thread" RUNNING {1004DC01F3}> expecting #<SB-THREAD:THREAD "lparallel" RUNNING {1003F8F073}>
Typical cause is: new threads were created with implementation-specific functions, as for example (sb-thread:make-thread) or (mp:process-run-function), that do not apply thread-local bindings stored in bordeaux-threads:default-special-bindings
Solution: use (bordeaux-threads:make-thread) instead.
问题根源
报错信息已经说得很清楚:lparallel默认使用SBCL原生的线程创建函数(sb-thread:make-thread),而stmx依赖bordeaux-threads(简称bt)的线程局部绑定机制来维护stmx.lang:*current-thread*变量。原生线程创建函数不会自动继承bt的*default-special-bindings*,导致新线程里的*current-thread*还是主线程的旧值,从而触发错误。
我一开始尝试了一个逻辑冗余的临时修复:
(lparallel:future (bt:make-thread (lambda () (stmx:atomic (format t "~a ~%" "it works")))))
这个方法虽然能跑通,但相当于在lparallel的线程里再嵌套一个bt线程,完全没必要。
后来我又写了一个更可行但不够优雅的临时宏:
(defmacro lpar-atomic (&body body) `(let ((stmx.lang:*current-thread* (lparallel.thread-util:current-thread))) (stmx:atomic ,@body))) (lparallel:future (lpar-atomic (format t "~a ~%" "it works!")))
它能直接修正当前线程的*current-thread*绑定,但每次用atomic都要套这个宏,复用性很差。
更优雅的通用解决方案
其实lparallel提供了自定义线程创建方式的入口——lparallel:*make-thread*变量。我们可以把它设置为bt:make-thread,让lparallel直接用bordeaux-threads创建线程,这样就能自动继承bt的特殊绑定,从根源上解决问题。
完整的示例代码如下:
(ql:quickload :stmx) (ql:quickload :lparallel) (ql:quickload :bordeaux-threads) ;; 设置lparallel使用bordeaux-threads创建线程 (setf lparallel:*make-thread* #'bt:make-thread) ;; 确保stmx的*current-thread*被加入bt的默认特殊绑定(如果还没在里面的话) (unless (assoc 'stmx.lang:*current-thread* bt:*default-special-bindings*) (push '(stmx.lang:*current-thread* . (bt:current-thread)) bt:*default-special-bindings*)) ;; 创建kernel并运行future (setf lparallel:*kernel* (lparallel:make-kernel 4)) (lparallel:future (stmx:atomic (format t "~a ~%" "it works!")))
为什么这个方法有效?
lparallel:*make-thread*控制着lparallel创建工作线程的函数,改成bt:make-thread后,所有lparallel的工作线程都会通过bt创建。- bt的
make-thread会自动应用*default-special-bindings*里的变量绑定,我们把stmx.lang:*current-thread*加入其中后,每个新线程启动时都会自动将这个变量绑定到当前线程实例,彻底避免了stale值的问题。
这个方案不需要额外的宏包装,也没有线程嵌套,是符合库设计意图的通用解决方案。
内容的提问来源于stack exchange,提问作者leetwinski

