-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
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
Add a convenience object for expressing once-like / per-runtime patterns #55793
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,6 +2,13 @@ | |
|
||
const ThreadSynchronizer = GenericCondition{Threads.SpinLock} | ||
|
||
""" | ||
current_task() | ||
|
||
Get the currently running [`Task`](@ref). | ||
""" | ||
current_task() = ccall(:jl_get_current_task, Ref{Task}, ()) | ||
|
||
# Advisory reentrant lock | ||
""" | ||
ReentrantLock() | ||
|
@@ -570,3 +577,278 @@ end | |
import .Base: Event | ||
export Event | ||
end | ||
|
||
const PerStateInitial = 0x00 | ||
const PerStateHasrun = 0x01 | ||
const PerStateErrored = 0x02 | ||
const PerStateConcurrent = 0x03 | ||
|
||
""" | ||
OncePerProcess{T}(init::Function)() -> T | ||
|
||
Calling a `OncePerProcess` object returns a value of type `T` by running the | ||
function `initializer` exactly once per process. All concurrent and future | ||
calls in the same process will return exactly the same value. This is useful in | ||
code that will be precompiled, as it allows setting up caches or other state | ||
which won't get serialized. | ||
|
||
## Example | ||
|
||
```jldoctest | ||
julia> const global_state = Base.OncePerProcess{Vector{UInt32}}() do | ||
println("Making lazy global value...done.") | ||
return [Libc.rand()] | ||
end; | ||
LilithHafner marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
julia> (procstate = global_state()) |> typeof | ||
Making lazy global value...done. | ||
Vector{UInt32} (alias for Array{UInt32, 1}) | ||
|
||
julia> procstate === global_state() | ||
true | ||
|
||
julia> procstate === fetch(@async global_state()) | ||
true | ||
``` | ||
""" | ||
mutable struct OncePerProcess{T, F} | ||
value::Union{Nothing,T} | ||
@atomic state::UInt8 # 0=initial, 1=hasrun, 2=error | ||
@atomic allow_compile_time::Bool | ||
const initializer::F | ||
const lock::ReentrantLock | ||
|
||
function OncePerProcess{T,F}(initializer::F) where {T, F} | ||
once = new{T,F}(nothing, PerStateInitial, true, initializer, ReentrantLock()) | ||
ccall(:jl_set_precompile_field_replace, Cvoid, (Any, Any, Any), | ||
once, :value, nothing) | ||
ccall(:jl_set_precompile_field_replace, Cvoid, (Any, Any, Any), | ||
once, :state, PerStateInitial) | ||
return once | ||
end | ||
end | ||
OncePerProcess{T}(initializer::F) where {T, F} = OncePerProcess{T, F}(initializer) | ||
OncePerProcess(initializer) = OncePerProcess{Base.promote_op(initializer), typeof(initializer)}(initializer) | ||
@inline function (once::OncePerProcess{T})() where T | ||
state = (@atomic :acquire once.state) | ||
if state != PerStateHasrun | ||
(@noinline function init_perprocesss(once, state) | ||
state == PerStateErrored && error("OncePerProcess initializer failed previously") | ||
once.allow_compile_time || __precompile__(false) | ||
lock(once.lock) | ||
try | ||
state = @atomic :monotonic once.state | ||
if state == PerStateInitial | ||
once.value = once.initializer() | ||
elseif state == PerStateErrored | ||
error("OncePerProcess initializer failed previously") | ||
elseif state != PerStateHasrun | ||
error("invalid state for OncePerProcess") | ||
end | ||
catch | ||
state == PerStateErrored || @atomic :release once.state = PerStateErrored | ||
unlock(once.lock) | ||
rethrow() | ||
end | ||
state == PerStateHasrun || @atomic :release once.state = PerStateHasrun | ||
unlock(once.lock) | ||
nothing | ||
end)(once, state) | ||
end | ||
return once.value::T | ||
end | ||
|
||
function copyto_monotonic!(dest::AtomicMemory, src) | ||
i = 1 | ||
for j in eachindex(src) | ||
if isassigned(src, j) | ||
@atomic :monotonic dest[i] = src[j] | ||
#else | ||
# _unsetindex_atomic!(dest, i, src[j], :monotonic) | ||
end | ||
i += 1 | ||
end | ||
dest | ||
end | ||
|
||
function fill_monotonic!(dest::AtomicMemory, x) | ||
for i = 1:length(dest) | ||
@atomic :monotonic dest[i] = x | ||
end | ||
dest | ||
end | ||
|
||
|
||
# share a lock/condition, since we just need it briefly, so some contention is okay | ||
const PerThreadLock = ThreadSynchronizer() | ||
""" | ||
OncePerThread{T}(init::Function)() -> T | ||
|
||
Calling a `OncePerThread` object returns a value of type `T` by running the function | ||
`initializer` exactly once per thread. All future calls in the same thread, and | ||
concurrent or future calls with the same thread id, will return exactly the | ||
same value. The object can also be indexed by the threadid for any existing | ||
thread, to get (or initialize *on this thread*) the value stored for that | ||
thread. Incorrect usage can lead to data-races or memory corruption so use only | ||
if that behavior is correct within your library's threading-safety design. | ||
|
||
!!! warning | ||
It is not necessarily true that a Task only runs on one thread, therefore the value | ||
returned here may alias other values or change in the middle of your program. This function | ||
may get deprecated in the future. If initializer yields, the thread running the current | ||
task after the call might not be the same as the one at the start of the call. | ||
|
||
See also: [`OncePerTask`](@ref). | ||
|
||
## Example | ||
|
||
```jldoctest | ||
julia> const thread_state = Base.OncePerThread{Vector{UInt32}}() do | ||
println("Making lazy thread value...done.") | ||
return [Libc.rand()] | ||
end; | ||
|
||
julia> (threadvec = thread_state()) |> typeof | ||
Making lazy thread value...done. | ||
Vector{UInt32} (alias for Array{UInt32, 1}) | ||
|
||
julia> threadvec === fetch(@async thread_state()) | ||
true | ||
|
||
julia> threadvec === thread_state[Threads.threadid()] | ||
true | ||
``` | ||
""" | ||
mutable struct OncePerThread{T, F} | ||
@atomic xs::AtomicMemory{T} # values | ||
@atomic ss::AtomicMemory{UInt8} # states: 0=initial, 1=hasrun, 2=error, 3==concurrent | ||
const initializer::F | ||
|
||
function OncePerThread{T,F}(initializer::F) where {T, F} | ||
xs, ss = AtomicMemory{T}(), AtomicMemory{UInt8}() | ||
once = new{T,F}(xs, ss, initializer) | ||
ccall(:jl_set_precompile_field_replace, Cvoid, (Any, Any, Any), | ||
once, :xs, xs) | ||
ccall(:jl_set_precompile_field_replace, Cvoid, (Any, Any, Any), | ||
once, :ss, ss) | ||
return once | ||
end | ||
end | ||
OncePerThread{T}(initializer::F) where {T, F} = OncePerThread{T,F}(initializer) | ||
OncePerThread(initializer) = OncePerThread{Base.promote_op(initializer), typeof(initializer)}(initializer) | ||
@inline (once::OncePerThread)() = once[Threads.threadid()] | ||
@inline function getindex(once::OncePerThread, tid::Integer) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Calling, this getindex might be a bit too cute? Could just be an optional argument to the call method, i.e.: function (once::OncePerThread)(tid::Integer = Threads.threadid()) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I agree that function call syntax would be better. |
||
tid = Int(tid) | ||
ss = @atomic :acquire once.ss | ||
xs = @atomic :monotonic once.xs | ||
# n.b. length(xs) >= length(ss) | ||
if tid <= 0 || tid > length(ss) || (@atomic :acquire ss[tid]) != PerStateHasrun | ||
(@noinline function init_perthread(once, tid) | ||
local ss = @atomic :acquire once.ss | ||
local xs = @atomic :monotonic once.xs | ||
local len = length(ss) | ||
# slow path to allocate it | ||
nt = Threads.maxthreadid() | ||
0 < tid <= nt || throw(ArgumentError("thread id outside of allocated range")) | ||
if tid <= length(ss) && (@atomic :acquire ss[tid]) == PerStateErrored | ||
error("OncePerThread initializer failed previously") | ||
end | ||
newxs = xs | ||
newss = ss | ||
if tid > len | ||
# attempt to do all allocations outside of PerThreadLock for better scaling | ||
@assert length(xs) >= length(ss) "logical constraint violation" | ||
newxs = typeof(xs)(undef, len + nt) | ||
newss = typeof(ss)(undef, len + nt) | ||
end | ||
# uses state and locks to ensure this runs exactly once per tid argument | ||
lock(PerThreadLock) | ||
try | ||
ss = @atomic :monotonic once.ss | ||
xs = @atomic :monotonic once.xs | ||
if tid > length(ss) | ||
@assert len <= length(ss) <= length(newss) "logical constraint violation" | ||
fill_monotonic!(newss, PerStateInitial) | ||
xs = copyto_monotonic!(newxs, xs) | ||
ss = copyto_monotonic!(newss, ss) | ||
@atomic :release once.xs = xs | ||
@atomic :release once.ss = ss | ||
end | ||
state = @atomic :monotonic ss[tid] | ||
while state == PerStateConcurrent | ||
# lost race, wait for notification this is done running elsewhere | ||
wait(PerThreadLock) # wait for initializer to finish without releasing this thread | ||
ss = @atomic :monotonic once.ss | ||
state = @atomic :monotonic ss[tid] | ||
end | ||
if state == PerStateInitial | ||
# won the race, drop lock in exchange for state, and run user initializer | ||
@atomic :monotonic ss[tid] = PerStateConcurrent | ||
result = try | ||
unlock(PerThreadLock) | ||
once.initializer() | ||
catch | ||
lock(PerThreadLock) | ||
ss = @atomic :monotonic once.ss | ||
@atomic :release ss[tid] = PerStateErrored | ||
notify(PerThreadLock) | ||
rethrow() | ||
end | ||
# store result and notify waiters | ||
lock(PerThreadLock) | ||
xs = @atomic :monotonic once.xs | ||
@atomic :release xs[tid] = result | ||
ss = @atomic :monotonic once.ss | ||
@atomic :release ss[tid] = PerStateHasrun | ||
notify(PerThreadLock) | ||
elseif state == PerStateErrored | ||
error("OncePerThread initializer failed previously") | ||
elseif state != PerStateHasrun | ||
error("invalid state for OncePerThread") | ||
end | ||
finally | ||
unlock(PerThreadLock) | ||
end | ||
nothing | ||
end)(once, tid) | ||
xs = @atomic :monotonic once.xs | ||
end | ||
return xs[tid] | ||
end | ||
|
||
""" | ||
OncePerTask{T}(init::Function)() -> T | ||
|
||
Calling a `OncePerTask` object returns a value of type `T` by running the function `initializer` | ||
exactly once per Task. All future calls in the same Task will return exactly the same value. | ||
|
||
See also: [`task_local_storage`](@ref). | ||
|
||
## Example | ||
|
||
```jldoctest | ||
julia> const task_state = Base.OncePerTask{Vector{UInt32}}() do | ||
println("Making lazy task value...done.") | ||
return [Libc.rand()] | ||
end; | ||
|
||
julia> (taskvec = task_state()) |> typeof | ||
Making lazy task value...done. | ||
Vector{UInt32} (alias for Array{UInt32, 1}) | ||
|
||
julia> taskvec === task_state() | ||
true | ||
|
||
julia> taskvec === fetch(@async task_state()) | ||
Making lazy task value...done. | ||
false | ||
``` | ||
""" | ||
mutable struct OncePerTask{T, F} | ||
const initializer::F | ||
|
||
OncePerTask{T}(initializer::F) where {T, F} = new{T,F}(initializer) | ||
OncePerTask{T,F}(initializer::F) where {T, F} = new{T,F}(initializer) | ||
OncePerTask(initializer) = new{Base.promote_op(initializer), typeof(initializer)}(initializer) | ||
end | ||
@inline (once::OncePerTask)() = get!(once.initializer, task_local_storage(), once) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should probably not be exported?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
*This = OncePerThread only. And still public, just not exported.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That seems fairly arbitrary?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's based on the idea that OncePerThread should be used very rarely (perhaps never in the absence of FFI)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Though, yes, it is fairly arbitrary, and I see the appeal to putting all three at the same level.