Yi *_*Chu 4 parallel-processing multithreading julia
我试图了解 Julia 中的多线程行为,并且注意到以下两个代码块在 Julia v1.6.3 中的行为有所不同(我在某个 script.jl 中的 Atom 中运行 Julia):
acc = 0
Threads.@threads for i in 1:1000
global acc
println(Threads.threadid(), ",", acc)
acc += 1
end
acc
Run Code Online (Sandbox Code Playgroud)
和
acc = 0
Threads.@threads for i in 1:1000
global acc
acc += 1
end
acc
Run Code Online (Sandbox Code Playgroud)
请注意,唯一的区别是我在后一种情况下去掉了“println(Threads.threadid(),”,”, acc)”。结果,每次运行第一个块都会给我 1000,第二个块会给我一些 <1000 的数字(由于竞争条件)。
我对 Julia 的并行计算(或一般的并行计算)完全陌生,因此希望能对这里发生的情况以及为什么单个打印行改变代码块的行为做出任何解释。
acc
您有多个线程同时改变状态,最终会出现竞争条件。
然而,println
与加法运算相比,它需要相对较长的println
时间,并且及时发生,因此对于小循环,您很有可能观察到“正确”的结果。但是,您的两个循环都不正确。
当许多线程改变完全相同的共享状态时,您需要引入锁定或使用原子变量。
SpinLock
:julia> acc = 0;
julia> u = Threads.SpinLock();
julia> Threads.@threads for i in 1:1000
global acc
Threads.lock(u) do
acc += 1
end
end
julia> acc
1000
Run Code Online (Sandbox Code Playgroud)
ReentrantLock
通常更适合运行时间较长的循环(与 相比,切换需要更长的SpinLock
时间),并且循环步骤内的时间不同(它不像 那样需要 CPU 时间“旋转” SpinLock
):julia> acc = 0
0
julia> u = ReentrantLock();
julia> Threads.@threads for i in 1:1000
global acc
Threads.lock(u) do
acc += 1
end
end
julia> acc
1000
Run Code Online (Sandbox Code Playgroud)
Atomic
):julia> acc2 = Threads.Atomic{Int}(0)
Base.Threads.Atomic{Int64}(0)
julia> Threads.@threads for i in 1:1000
global acc2
Threads.atomic_add!(acc2, 1)
end
julia> acc2[]
1000
Run Code Online (Sandbox Code Playgroud)
您可能知道这一点,但在现实生活中,所有这些都应该在一个函数中;如果您使用全局变量,您的性能将是灾难性的,而使用函数时,您只需单线程实现就会领先数英里。虽然“慢速”编程语言的用户通常会立即寻求并行性来提高性能,但对于 Julia,通常最好的方法是首先分析单线程实现的性能(使用分析器等工具)并修复您发现的任何问题。特别是对于 Julia 的新手来说,通过这种方式使代码速度提高十倍或一百倍并不罕见,在这种情况下,您可能会觉得这就是您所需要的。
\n事实上,有时单线程实现会更快,因为线程引入了它自己的开销。我们可以在这里轻松地说明这一点。我将对上面的代码进行一项修改:我将添加 ,而不是在每次迭代中添加 1 i % 2
,如果i
是奇数则添加 1,如果i
是偶数则添加 0。我这样做是因为一旦你把它放入一个函数中,如果你所做的只是加 1,Julia 的编译足够聪明,可以弄清楚你在做什么,然后返回答案,而无需实际运行循环; 我们想要运行循环,所以我们必须让它变得有点棘手,这样编译器就无法提前找出答案。
首先,让我们尝试上面最快的线程实现(我启动 Juliajulia -t4
使用 4 个线程):
julia> acc2 = Threads.Atomic{Int}(0)\nBase.Threads.Atomic{Int64}(0)\n\njulia> @btime Threads.@threads for i in 1:1000\n global acc2\n Threads.atomic_add!(acc2, i % 2)\n end\n 12.983 \xce\xbcs (21 allocations: 1.86 KiB)\n\njulia> @btime Threads.@threads for i in 1:1000000\n global acc2\n Threads.atomic_add!(acc2, i % 2)\n end\n 27.532 ms (22 allocations: 1.89 KiB)\n
Run Code Online (Sandbox Code Playgroud)\n这是快还是慢?让我们首先将其放入一个函数中,看看它是否有帮助:
\njulia> function lockadd(n)\n acc = Threads.Atomic{Int}(0)\n Threads.@threads for i = 1:n\n Threads.atomic_add!(acc, i % 2)\n end\n return acc[]\n end\nlockadd (generic function with 1 method)\n\njulia> @btime lockadd(1000)\n 9.737 \xce\xbcs (22 allocations: 1.88 KiB)\n500\n\njulia> @btime lockadd(1000000)\n 13.356 ms (22 allocations: 1.88 KiB)\n500000\n
Run Code Online (Sandbox Code Playgroud)\n因此,通过将其放入函数中,我们获得了 2 倍的系数(在更大的工作中)。然而,更好的线程策略是无锁线程:为每个线程提供自己的线程acc
,然后accs
在最后添加所有单独的线程。
julia> function threadedadd(n)\n accs = zeros(Int, Threads.nthreads())\n Threads.@threads for i = 1:n\n accs[Threads.threadid()] += i % 2\n end\n return sum(accs)\n end\nthreadedadd (generic function with 1 method)\n\njulia> using BenchmarkTools\n\njulia> @btime threadedadd(1000)\n 2.967 \xce\xbcs (22 allocations: 1.97 KiB)\n500\n\njulia> @btime threadedadd(1000000)\n 56.852 \xce\xbcs (22 allocations: 1.97 KiB)\n500000\n
Run Code Online (Sandbox Code Playgroud)\n对于更长的循环,我们获得了超过 200 倍的性能!这确实是一个非常好的加速。
\n不过,让我们尝试一个简单的单线程实现:
\njulia> function addacc(n)\n acc = 0\n for i in 1:n\n acc += i % 2\n end\n return acc\n end\naddacc (generic function with 1 method)\n\njulia> @btime addacc(1000)\n 43.218 ns (0 allocations: 0 bytes)\n500\n\njulia> @btime addacc(1000000)\n 41.068 \xce\xbcs (0 allocations: 0 bytes)\n500000\n
Run Code Online (Sandbox Code Playgroud)\n这比小型作业上的线程实现快 70 倍,甚至在大型作业上也更快。为了完整起见,让我们将其与使用全局状态的相同代码进行比较:
\njulia> @btime for i in 1:1000\n global acc\n acc += i % 2\n end\n 20.158 \xce\xbcs (1000 allocations: 15.62 KiB)\n\njulia> @btime for i in 1:1000000\n global acc\n acc += i % 2\n end\n 20.455 ms (1000000 allocations: 15.26 MiB)\n
Run Code Online (Sandbox Code Playgroud)\n太可怕了。
\n当然,在某些情况下并行性会产生影响,但它通常适用于更复杂的任务。除非您已经优化了单线程实现,否则您仍然不应该使用它。
\n所以这个故事有两个重要的寓意:
\n 归档时间: |
|
查看次数: |
2599 次 |
最近记录: |