Just added a Mutex feature for safely working with shared data!
This might be the most practical feature added so far. It lets you do multi-threaded writes in a very safe way. (The rules are inspired from Rust’s std::sync::Mutex
)
So, right off the bat, since a Mutex
object is safe to share an unlimited number of times (it’s basically an immutable reference that can generate mutable references under a lock), you no longer need the verbose @own
macro!
julia> m = Mutex([1, 2, 3])
Mutex{Vector{Int64}}([1, 2, 3])
julia> m2 = m # this is discouraged with `Owned` objects, but here, it's safe
You can use regular Julia assignment syntax with this. You can capture it in a closure, put it a million times into a vector, etc.
This is because, to actually edit the protected object, you need to first hold the lock:
julia> lock(m);
julia> @ref_into :mut data = m[]
BorrowedMut{Vector{Int64},OwnedMut{Vector{Int64}}}([1, 2, 3, 4], :data)
julia> push!(data, 4);
julia> unlock(m);
julia> m
Mutex{Vector{Int64}}([1, 2, 3, 4])
The beauty of this is that the locking-unlocking determines the lifetime. Once you release the lock, all the references you created to the guarded value expire! You can’t touch the data anymore through those references:
julia> data
[expired reference]
julia> data[1]
ERROR: Cannot use `data`: value's lifetime has expired
In other words, it has self-destructing references! This makes it very safe to use.
Even if you are good about making a lock around your mutable container, there is always a chance that it becomes unprotected from the lock at some point (maybe gets stored in some closure or other mutable container, etc.). The Borrowed
objects are nice because they can act like a normal object, but have their access to data revoked at any time once the lifetime expires! And the new Mutex
ensures the lifetime only lasts as long as the lock is held.
Of course, this isn’t doing this all at a compiler level (maybe one day :-)), but I think it’s quite a nice model for safe access.
For a multi-threaded example, showing an arbitrary mutable struct:
julia> mutable struct A
a::Int
end
julia> m = Mutex(A(0))
Mutex{A}(A(0))
julia> @sync for i in 1:100
Threads.@spawn begin
Base.@lock m begin
@ref_into :mut r = m[]
r.a += 1
end
end
end
julia> m
Mutex{A}(A(100))
All those references will expire (useful since they get stored in the closure generated by Threads.@spawn
! We also can’t access the inner value without a lock:
julia> m[]
ERROR: LockNotHeldError: Current task does not hold the lock for this Mutex.
Access via `m[]` is only allowed when the lock is held by the current task.
@Mason this might also be the right way to integrate it in Bumper.jl. Once the bump allocation arena is exited, you could end the lifetime, and that will kills off any references to the array storage that might be hanging around.