为何Julia多线程累加代码添加println后未出现竞态条件
问题本质解释
两段代码都存在竞态条件,第一段代码添加println后能得到1000只是测试规模下的巧合,并非真正实现了线程安全。
acc += 1本身是非原子操作:它的执行分为三个独立步骤:读取内存中acc的当前值、对值执行加1运算、将新值写回acc的内存地址。当两个线程同时完成第一步读取操作时,后续会各自计算加1后写回,彼此覆盖结果,最终总累加次数就会小于1000,这就是第二段代码的问题来源。println操作大幅降低了竞态触发概率:- Julia的标准输出流默认是线程安全的,内部内置了互斥锁,同一时间仅允许一个线程执行打印逻辑,相当于在累加操作前强制让线程串行执行了打印环节。
- 打印属于IO操作,耗时是纯内存算术运算的数千到数万倍,极大拉长了每个线程执行累加操作的时间窗口,多个线程同时读取
acc值的概率被降到极低,在你测试的1000次循环规模下几乎不会触发冲突,所以每次运行都得到1000。
- 注意:这不是线程安全的解决方案,如果将循环规模放大到1e6甚至更高,哪怕保留
println语句,依然会出现最终acc小于循环次数的情况。
正确的线程安全累加实现
要彻底避免竞态条件,可以用以下两种常见方案:
- 使用原子变量保证累加操作的原子性
acc = Threads.Atomic{Int}(0) Threads.@threads for i in 1:1000 Threads.atomic_add!(acc, 1) end # 读取原子变量的值需要用 [] @assert acc[] == 1000
- 使用线程局部存储先各自累加,最后汇总结果
# 每个线程对应一个局部计数器,避免写冲突 local_counters = zeros(Int, Threads.nthreads()) Threads.@threads for i in 1:1000 local_counters[Threads.threadid()] += 1 end acc = sum(local_counters) @assert acc == 1000
内容的提问来源于stack exchange,提问作者Yi Chu
相关产品推荐
相关产品推荐

