From b494d788953d42e1a6e6cc1677e7bbaf8fea8351 Mon Sep 17 00:00:00 2001 From: Dennis Hoelgaard Bal <61620837+KronosTheLate@users.noreply.github.com> Date: Mon, 14 Apr 2025 16:49:38 +0200 Subject: [PATCH 1/7] Mention Base.Lockable in "multi-threading.md" When I originally learnt about multithreading and locks (and ended up rewriting [https://docs.julialang.org/en/v1/manual/multi-threading/#man-using-locks](Using locks to avoid data-races)), I was puzzled to find that the association of a lock and a value was the mental task of the programmer, and not the programmatic task of the program. I was therefore very happy to see Base.Lockable introduced in Julia 1.11. What was missing, was any mention of it in the relevant section of the manual. This PR adds a subsection under locks (So a forth level of headings, not sure if that is fine), showcasing and reccomending the use of Base.Lockable. I have made heavy use of comments in the example, which breaks with the general style, but adds valuable interpretation along the way. I am very open restructuring that part in particular. --- doc/src/manual/multi-threading.md | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/doc/src/manual/multi-threading.md b/doc/src/manual/multi-threading.md index ec470f867cc47..eed6c77b8090a 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 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. 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,32 @@ 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. + + +```julia-repl +julia> my_array = []; # Simple empty array + +julia> my_locked_array = Base.Lockable(my_array); # The lock type defaults toReentrantLock(), which is fine in most cases + +julia> lock(identity, my_locked_array) # The first argument is a function that is applied to the "unlocked" array, so `identity` is good for inspecting the associated value in a thread-safe manner +Any[] + +julia> lock(my_locked_array) do x # The functional version of lock` along with the do-syntax` is a convenient way to work with a Lockable object + push!(x, 1) + end; + +julia> lock(identity, my_locked_array) # The array is now mutated, without any risk of data-races +1-element Vector{Any}: + 1 + +julia> my_array # The original array is identical to the one contained in my_locked_array`` +1-element Vector{Any}: + 1 + +``` + ### [Atomic Operations](@id man-atomic-operations) Julia supports accessing and modifying values *atomically*, that is, in a thread-safe way to avoid From fc1d649061b69f859dd172bb25c4701148a3af7c Mon Sep 17 00:00:00 2001 From: Chengyu Han Date: Tue, 29 Apr 2025 23:28:49 +0800 Subject: [PATCH 2/7] Update doc/src/manual/multi-threading.md --- doc/src/manual/multi-threading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/manual/multi-threading.md b/doc/src/manual/multi-threading.md index eed6c77b8090a..b49740b4feea6 100644 --- a/doc/src/manual/multi-threading.md +++ b/doc/src/manual/multi-threading.md @@ -301,7 +301,7 @@ 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. +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. 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). From 56a85e2463b5b4a3203ca4f916037e787e73b2c6 Mon Sep 17 00:00:00 2001 From: Dennis Hoelgaard Bal <61620837+KronosTheLate@users.noreply.github.com> Date: Mon, 2 Jun 2025 09:08:59 +0200 Subject: [PATCH 3/7] Rewrite Using Base.Lockable to associate a lock and a value --- doc/src/manual/multi-threading.md | 42 +++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 13 deletions(-) diff --git a/doc/src/manual/multi-threading.md b/doc/src/manual/multi-threading.md index b49740b4feea6..a1492cb3c1eaa 100644 --- a/doc/src/manual/multi-threading.md +++ b/doc/src/manual/multi-threading.md @@ -342,27 +342,43 @@ to a global or closure variable) accessed by other threads. Failing to do this c #### [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 = []; # Simple empty array - -julia> my_locked_array = Base.Lockable(my_array); # The lock type defaults toReentrantLock(), which is fine in most cases +julia> my_array = []; -julia> lock(identity, my_locked_array) # The first argument is a function that is applied to the "unlocked" array, so `identity` is good for inspecting the associated value in a thread-safe manner -Any[] - -julia> lock(my_locked_array) do x # The functional version of lock` along with the do-syntax` is a convenient way to work with a Lockable object - push!(x, 1) - end; +julia> my_locked_array = Base.Lockable(my_array); +``` -julia> lock(identity, my_locked_array) # The array is now mutated, without any risk of data-races +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 +``` -julia> my_array # The original array is identical to the one contained in my_locked_array`` -1-element Vector{Any}: +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 in the background. Anonymus functions, named functions, and do-blocks can all work well here: +```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) From 93ae8bae5fb3fcf56e169a7f9e195670a165bfba Mon Sep 17 00:00:00 2001 From: Dennis Hoelgaard Bal <61620837+KronosTheLate@users.noreply.github.com> Date: Tue, 3 Jun 2025 08:44:28 +0200 Subject: [PATCH 4/7] Update doc/src/manual/multi-threading.md Co-authored-by: Neven Sajko <4944410+nsajko@users.noreply.github.com> --- doc/src/manual/multi-threading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/manual/multi-threading.md b/doc/src/manual/multi-threading.md index a1492cb3c1eaa..27357630bb967 100644 --- a/doc/src/manual/multi-threading.md +++ b/doc/src/manual/multi-threading.md @@ -363,7 +363,7 @@ julia> begin 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 in the background. Anonymus functions, named functions, and do-blocks can all work well here: +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 in the background. Anonymous functions, named functions, and do-blocks can all work well here: ```julia-repl julia> lock(x -> push!(x, 2), my_locked_array); From 6b6ccabec2451280ad7f4dee8e6093aca569ee8e Mon Sep 17 00:00:00 2001 From: Dennis Hoelgaard Bal <61620837+KronosTheLate@users.noreply.github.com> Date: Wed, 18 Jun 2025 13:44:31 +0200 Subject: [PATCH 5/7] Update doc/src/manual/multi-threading.md Co-authored-by: Neven Sajko <4944410+nsajko@users.noreply.github.com> --- doc/src/manual/multi-threading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/manual/multi-threading.md b/doc/src/manual/multi-threading.md index 27357630bb967..17bc46f0e133c 100644 --- a/doc/src/manual/multi-threading.md +++ b/doc/src/manual/multi-threading.md @@ -363,7 +363,7 @@ julia> begin 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 in the background. Anonymous functions, named functions, and do-blocks can all work well here: +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); From 5a95ecbda5d715b71fd517cbfbe773adb401370e Mon Sep 17 00:00:00 2001 From: Dennis Hoelgaard Bal <61620837+KronosTheLate@users.noreply.github.com> Date: Fri, 20 Jun 2025 17:16:34 +0200 Subject: [PATCH 6/7] Update doc/src/manual/multi-threading.md Co-authored-by: Neven Sajko <4944410+nsajko@users.noreply.github.com> --- doc/src/manual/multi-threading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/manual/multi-threading.md b/doc/src/manual/multi-threading.md index 17bc46f0e133c..32d72bb13945d 100644 --- a/doc/src/manual/multi-threading.md +++ b/doc/src/manual/multi-threading.md @@ -301,7 +301,7 @@ 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. +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. 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). From b5479cc4d9fe6740ebf333816f68dbecad420ee6 Mon Sep 17 00:00:00 2001 From: Dennis Hoelgaard Bal <61620837+KronosTheLate@users.noreply.github.com> Date: Fri, 20 Jun 2025 17:17:35 +0200 Subject: [PATCH 7/7] Commit suggestion --- doc/src/manual/multi-threading.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/src/manual/multi-threading.md b/doc/src/manual/multi-threading.md index 32d72bb13945d..17ad6123f6034 100644 --- a/doc/src/manual/multi-threading.md +++ b/doc/src/manual/multi-threading.md @@ -301,7 +301,7 @@ 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. +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).