Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ Multi-threading changes
-----------------------

* `Threads.@threads` now supports the `:greedy` scheduler, intended for non-uniform workloads ([#52096]).
* A new exported struct `Lockable{T, L<:AbstractLock}` makes it easy to bundle a resource and its lock together ([#52898]).

Build system changes
--------------------
Expand Down
9 changes: 4 additions & 5 deletions base/env.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,18 @@
if Sys.iswindows()
const ERROR_ENVVAR_NOT_FOUND = UInt32(203)

const env_dict = Dict{String, Vector{Cwchar_t}}()
const env_lock = ReentrantLock()
const env_dict = Lockable(Dict{String, Vector{Cwchar_t}}())

function memoized_env_lookup(str::AbstractString)
# Windows environment variables have a different format from Linux / MacOS, and previously
# incurred allocations because we had to convert a String to a Vector{Cwchar_t} each time
# an environment variable was looked up. This function memoizes that lookup process, storing
# the String => Vector{Cwchar_t} pairs in env_dict
@lock env_lock begin
var = get(env_dict, str, nothing)
@lock env_dict begin
var = get(env_dict[], str, nothing)
if isnothing(var)
var = cwstring(str)
env_dict[str] = var
env_dict[][str] = var
end
return var
end
Expand Down
57 changes: 57 additions & 0 deletions base/lock.jl
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,63 @@ macro lock_nofail(l, expr)
end
end

"""
Lockable(value, lock = ReentrantLock())

Creates a `Lockable` object that wraps `value` and
associates it with the provided `lock`. This object
supports [`@lock`](@ref), [`lock`](@ref), [`trylock`](@ref),
[`unlock`](@ref). To access the value, index the lockable object while
holding the lock.

!!! compat "Julia 1.11"
Requires at least Julia 1.11.

## Example

```jldoctest
julia> locked_list = Base.Lockable(Int[]);

julia> @lock(locked_list, push!(locked_list[], 1)) # must hold the lock to access the value
1-element Vector{Int64}:
1

julia> lock(summary, locked_list)
"1-element Vector{Int64}"
```
"""
struct Lockable{T, L <: AbstractLock}
value::T
lock::L
end

Lockable(value) = Lockable(value, ReentrantLock())
getindex(l::Lockable) = (assert_havelock(l.lock); l.value)

"""
lock(f::Function, l::Lockable)

Acquire the lock associated with `l`, execute `f` with the lock held,
and release the lock when `f` returns. `f` will receive one positional
argument: the value wrapped by `l`. If the lock is already locked by a
different task/thread, wait for it to become available.
When this function returns, the `lock` has been released, so the caller should
not attempt to `unlock` it.

!!! compat "Julia 1.11"
Requires at least Julia 1.11.
"""
function lock(f, l::Lockable)
lock(l.lock) do
f(l.value)
end
end

# implement the rest of the Lock interface on Lockable
lock(l::Lockable) = lock(l.lock)
trylock(l::Lockable) = trylock(l.lock)
unlock(l::Lockable) = unlock(l.lock)

@eval Threads begin
"""
Threads.Condition([lock])
Expand Down
1 change: 1 addition & 0 deletions doc/src/base/parallel.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ Base.trylock
Base.islocked
Base.ReentrantLock
Base.@lock
Base.Lockable
```

## Channels
Expand Down
30 changes: 30 additions & 0 deletions test/misc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,36 @@ let l = ReentrantLock()
@test_throws ErrorException unlock(l)
end

# Lockable{T, L<:AbstractLock}
using Base: Lockable
let
@test_broken Base.isexported(Base, :Lockable)
lockable = Lockable(Dict("foo" => "hello"), ReentrantLock())
# note field access is non-public
@test lockable.value["foo"] == "hello"
@test @lock(lockable, lockable[]["foo"]) == "hello"
lock(lockable) do d
@test d["foo"] == "hello"
end
lock(lockable) do d
d["foo"] = "goodbye"
end
@test lockable.value["foo"] == "goodbye"
@lock lockable begin
@test lockable[]["foo"] == "goodbye"
end
l = trylock(lockable)
try
@test l
finally
unlock(lockable)
end
# Test 1-arg constructor
lockable2 = Lockable(Dict("foo" => "hello"))
@test lockable2.lock isa ReentrantLock
@test @lock(lockable2, lockable2[]["foo"]) == "hello"
end

for l in (Threads.SpinLock(), ReentrantLock())
@test get_finalizers_inhibited() == 0
@test lock(get_finalizers_inhibited, l) == 1
Expand Down