diff --git a/doc/src/manual/multi-threading.md b/doc/src/manual/multi-threading.md index ec470f867cc47..17ad6123f6034 100644 --- a/doc/src/manual/multi-threading.md +++ b/doc/src/manual/multi-threading.md @@ -301,7 +301,9 @@ bad_read2(a) # it is NOT safe to access `a` here ``` ### [Using locks to avoid data-races](@id man-using-locks) -An important tool to avoid data-races, and thereby write thread-safe code, is the concept of a "lock". A lock can be locked and unlocked. If a thread has locked a lock, and not unlocked it, it is said to "hold" the lock. If there is only one lock, and we write code the requires holding the lock to access some data, we can ensure that multiple threads will never access the same data simultaneously. Note that the link between a lock and a variable is made by the programmer, and not the program. +An important tool for avoiding data races, and writing thread-safe code in general, is the concept of a "lock". A lock can be locked and unlocked. If a thread has locked a lock, and not unlocked it, it is said to "hold" the lock. If there is only one lock, and we write code the requires holding the lock to access some data, we can ensure that multiple threads will never access the same data simultaneously. + +Note that the link between a lock and a variable is made by the programmer, and not the program. A helper-type [`Base.Lockable`](@ref) exists that helps you associate a lock and a value. This is often more safe than keeping track yourself, and is detailed under [Using Base.Lockable to associate a lock and a value](@ref man-lockable). For example, we can create a lock `my_lock`, and lock it while we mutate a variable `my_variable`. This is done most simply with the `@lock` macro: @@ -337,6 +339,48 @@ julia> begin All three options are equivalent. Note how the final version requires an explicit `try`-block to ensure that the lock is always unlocked, whereas the first two version do this internally. One should always use the lock pattern above when changing data (such as assigning to a global or closure variable) accessed by other threads. Failing to do this could have unforeseen and serious consequences. +#### [Using Base.Lockable to associate a lock and a value](@id man-lockable) +As mentioned in the previous section, the helper-type [`Base.Lockable`](@ref) can be used to programmatically ensure the association between a lock and a value. This is generally recommended, as it is both less prone to error and more readable for others compared to having the association only by convention. + +Any object can be wrapped in `Base.Lockable`: +```julia-repl +julia> my_array = []; + +julia> my_locked_array = Base.Lockable(my_array); +``` + +If the lock is held, the underlying object can be accessed with the empty indexing notation: +```julia-repl +julia> begin + lock(my_locked_array) + try + push!(my_locked_array[], 1) + finally + unlock(my_locked_array) + end + end +1-element Vector{Any}: + 1 +``` + +It is usually easier and safer to pass a function as the first argument to `lock`. The function is applied to the unlocked object, and the locking/unlocking is handled automatically: +```julia-repl +julia> lock(x -> push!(x, 2), my_locked_array); + +julia> lock(display, my_locked_array) +2-element Vector{Any}: + 1 + 2 + +julia> lock(my_locked_array) do x + x[1] = π + display(x) + end +2-element Vector{Any}: + π = 3.1415926535897... + 2 +``` + ### [Atomic Operations](@id man-atomic-operations) Julia supports accessing and modifying values *atomically*, that is, in a thread-safe way to avoid