Introduction
In order to secure a mutable variable from concurrent access by multiple threads and/or distributed workers you call guard
on it. That wraps it into a :guard
actor represented only by a Guard
link.
Then it can be accessed only by message passing via cast
and call
over that link. Calls to it return deep copies. You can still modify a guarded variable by sending the :guard
actor a modifier function and arguments to it.
The @grd
macro expands a call to the link into a synchronous call
request.
Example
julia> using Guards
julia> gd = guard([1,2,3]) # start a guard actor around an array
Guards.Guard{Array{Int64,1}}(Link{Channel{Any}}(Channel{Any}(sz_max:32,sz_curr:0), 1, :guard))
julia> call(gd) # get a deep copy of it
3-element Array{Int64,1}:
1
2
3
julia> push!(call(gd), 4) # pushing to it ...
4-element Array{Int64,1}:
1
2
3
4
julia> call(gd) # the guarded variable has not changed
3-element Array{Int64,1}:
1
2
3
julia> call(gd, push!, 4); # if you call it with push!,
julia> @grd gd # ... it got changed (here using the @grd macro)
4-element Array{Int64,1}:
1
2
3
4
julia> @grd pop!(gd) # pop! with the macro
4
julia> update!(gd, [5,6,7,8])
4-element Array{Int64,1}:
5
6
7
8
julia> @grd gd
4-element Array{Int64,1}:
5
6
7
8
Multithreading
Even if with actors we avoid race conditions, concurrency is still challenging. Consider the following where 8 threads concurrently try to increment a guarded variable:
julia> using .Threads
julia> gd = guard(zeros(Int, 10))
Guard{Array{Int64,1}}(Link{Channel{Any}}(Channel{Any}(sz_max:32,sz_curr:0), 1, :guard))
julia> for i in 1:10
@threads for _ in 1:nthreads()
gd[i] += 1
end
end
julia> @grd gd
10-element Array{Int64,1}:
1
1
1
1
1
1
1
1
1
1
What has happened? gd[i] += 1
is not a single actor call. First all 8 threads get 0 by doing getindex(var, i)
and then they do setindex!(var, i, 0+1)
on it. The result is 1 and not 8 as we would expect. In order to get it right we create a function:
julia> incr(arr, index, by) = arr[index] += by
incr (generic function with 1 method)
julia> gd = guard(zeros(Int, 10))
Guard{Array{Int64,1}}(Link{Channel{Any}}(Channel{Any}(sz_max:32,sz_curr:0), 1, :guard))
julia> for i in 1:10
@threads for _ in 1:nthreads()
@grd incr(gd, i, 1)
end
end
julia> @grd gd
10-element Array{Int64,1}:
8
8
8
8
8
8
8
8
8
8
Thus the guard actor receives nthreads()
calls to incr
for each i
and it works as expected.
Distributed
For distributed computing we can create named guards or guards with remote links. All worker processes can work with the same guarded variable:
julia> using Distributed
julia> addprocs(1);
julia> @everywhere using Guards
julia> gd = guard([1,2,3], remote=true) # a guard with a remote link
Guard{Array{Int64,1}}(Link{RemoteChannel{Channel{Any}}}(RemoteChannel{Channel{Any}}(1, 1, 13), 1, :guard))
julia> fetch(@spawnat 2 @grd gd) # show it on pid 2
3-element Array{Int64,1}:
1
2
3
julia> @fetchfrom 2 InteractiveUtils.varinfo()
name size summary
––––––––––– ––––––––––– –––––––––––––––––––––
Base Module
Core Module
Distributed 918.170 KiB Module
Main Module
gd 56 bytes Guard{Array{Int64,1}}
julia> @grd push!(gd, 4) # push! on pid 1
4-element Array{Int64,1}:
1
2
3
4
julia> @spawnat 2 @grd push!(gd, 5) # push on pid 2
Future(2, 1, 20, nothing)
julia> @grd gd # it is everywhere up to date
5-element Array{Int64,1}:
1
2
3
4
5
If we send local guarded variables to distributed actors or if we create distributed actors with guarded variables as arguments, their local links are automatically converted to remote ones, so they can work with them.