RestrictedPython 线程池并行执行时偶现代码重复运行问题排查
问题根因
核心问题:全局共享执行上下文导致的竞态,和RestrictedPython本身无关
- 你定义的
_restricted_python_globals是进程级全局共享字典,所有工作线程没有做任何上下文隔离,直接复用同一个对象作为exec的命名空间。 - 目前锁的覆盖范围完全错位:你把全局锁加在了编译代码、往全局字典写入
dev参数的流程上,释放锁之后,所有线程拿同一个共享字典无锁跑exec,从根源上没有解决多线程读写同一命名空间的竞态问题。 - 偶发执行两次的现象本质是竞态交叉:
- 线程A拿到锁,编译完自身的代码,往全局字典里写入自己的设备实例
dev_A,释放锁准备执行exec - 线程B拿到锁,编译完自身的代码,往同一个全局字典里写入自己的设备实例
dev_B,直接覆盖掉之前的dev_A,释放锁准备执行exec - GIL调度切换时,两个线程的执行流交叉读写同一个命名空间,会出现参数串用、执行逻辑交叉的现象,最终让观测结果表现为“代码被重复执行两次”——实际上是两个不同线程的执行流混到了同一个上下文里,不是RestrictedPython本身重复运行了同一段代码。
- 线程A拿到锁,编译完自身的代码,往全局字典里写入自己的设备实例
- 额外的隐患:如果用户写的自定义脚本里定义了顶层变量、函数,这些内容会直接写入全局共享字典,后续启动的线程会读到上一个线程留下的残留状态,进一步放大偶发的执行异常。
锁的逻辑完全多余
- RestrictedPython的
compile_restricted是纯函数,不依赖全局可变状态,多线程并行调用本身是线程安全的,根本不需要给编译流程加全局互斥锁。 - 你当前加的锁不仅没有解决核心问题,反而把本来可以并行的编译流程强行串行化,拉低了整体执行效率。
修复方案
1. 每个线程独立隔离执行上下文
- 不要复用全局的globals字典,把全局字典作为只读模板,每个线程执行前生成独立的深拷贝对象,再写入当前线程专属的设备实例,从根源上消除共享状态:
import copy # 模板只存无状态的钩子函数,不存任何可变实例 RESTRICTED_GLOBALS_TEMPLATE = { '__metaclass__': type, '_getattr_': _hook_getattr, '_getiter_': default_guarded_getiter, '_write_': _hook_writable, '_getitem_': default_guarded_getitem, } class RestrictedPythonThread(Thread): def run(self): # 每个线程独立拷贝globals,完全不跨线程共享 code_globals = copy.deepcopy(RESTRICTED_GLOBALS_TEMPLATE) code_globals['dev'] = self.dev code_globals['smart_device'] = None # 编译不需要全局锁,直接执行即可 code = _get_compiled_restricted_python(self.dev.device_epm.epm) # exec显式传独立的局部命名空间,避免作用域污染 exec(code, code_globals, {})
2. 移除无意义的全局互斥锁
- 现有的全局锁不仅挡不住执行阶段的竞态,还会拖慢并行速度。如果后续需要做编译缓存(避免同一个脚本重复编译),只需要给缓存字典加粒度匹配的锁即可,不要把整个编译流程用全局锁包裹。
3. 额外的安全校验
- 执行
exec时必须显式传入两个独立字典参数,第二个空字典作为局部命名空间,避免用户脚本的变量不小心污染外层作用域。 - 禁止在共享的globals模板里存放任何带状态的实例、数据库连接、文件句柄这类可变对象,所有带状态的参数必须由当前线程独立持有引用,不要跨线程传递共享。
RestrictedPython的线程安全说明
- 库的核心编译逻辑、内置的默认守卫函数都是无状态的,只要你不跨线程共享可变的执行上下文,多线程并行执行完全合规,不存在“必须单线程运行”的限制。
内容的提问来源于stack exchange,提问作者Symon
相关产品推荐
相关产品推荐

