Skip to content

Mention Base.Lockable in "multi-threading.md" #58107

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
46 changes: 45 additions & 1 deletion doc/src/manual/multi-threading.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down Expand Up @@ -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
Expand Down