-
-
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
base: master
Are you sure you want to change the base?
Changes from all commits
ccf1c8d
3876d43
49ead81
539d600
6099c23
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() | ||||||||||||
|
@@ -500,7 +507,7 @@ Create a level-triggered event source. Tasks that call [`wait`](@ref) on an | |||||||||||
After `notify` is called, the `Event` remains in a signaled state and | ||||||||||||
tasks will no longer block when waiting for it, until `reset` is called. | ||||||||||||
|
||||||||||||
If `autoreset` is true, at most one task will be released from `wait` for | ||||||||||||
If `autoreset` is true, at most one task will be released from `wait` for) | ||||||||||||
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. Mistake? 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.
Suggested change
|
||||||||||||
each call to `notify`. | ||||||||||||
|
||||||||||||
This provides an acquire & release memory ordering on notify/wait. | ||||||||||||
|
@@ -570,3 +577,274 @@ end | |||||||||||
import .Base: Event | ||||||||||||
export Event | ||||||||||||
end | ||||||||||||
|
||||||||||||
const PerStateInitial = 0x00 | ||||||||||||
const PerStateHasrun = 0x01 | ||||||||||||
const PerStateErrored = 0x02 | ||||||||||||
const PerStateConcurrent = 0x03 | ||||||||||||
|
||||||||||||
""" | ||||||||||||
OncePerProcess{T} | ||||||||||||
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.
Suggested change
This seems more suggestive of how this is actually used? |
||||||||||||
|
||||||||||||
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; | ||||||||||||
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.
Suggested change
Showing the return type here may be helpful. That will show us that |
||||||||||||
|
||||||||||||
julia> procstate = global_state(); | ||||||||||||
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. Showing the value here would be helpful. Will cause problems with the doctest, but generating random values is kind of a bad example anyway. Maybe we can brainstorm a more meaningful example... Would have been helpful (to me at least), to make clear that |
||||||||||||
Making lazy global value...done. | ||||||||||||
Comment on lines
+603
to
+604
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.
Suggested change
|
||||||||||||
|
||||||||||||
julia> procstate === global_state() | ||||||||||||
true | ||||||||||||
|
||||||||||||
julia> procstate === fetch(@async global_state()) | ||||||||||||
true | ||||||||||||
``` | ||||||||||||
""" | ||||||||||||
mutable struct OncePerProcess{T, F} | ||||||||||||
x::Union{Nothing,T} | ||||||||||||
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.
Suggested change
Calling this 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.
Suggested change
|
||||||||||||
@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, :x, 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.x = 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.x::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 | ||||||||||||
# _unsafeindex_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} | ||||||||||||
|
||||||||||||
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 type 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). | ||||||||||||
Comment on lines
+694
to
+699
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. Perhaps lead with this warning and the admonition to almost always use OncePerTask instead. 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. Should probably also be a 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.
Should it be under the |
||||||||||||
|
||||||||||||
## 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(); | ||||||||||||
Making lazy thread value...done. | ||||||||||||
|
||||||||||||
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 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()) |
||||||||||||
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 | ||||||||||||
@inline (once::OncePerThread)() = once[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 would put this above since it's the primary API. 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. Why not function once::OncePerThread(threadid=Threads.threadid())
...
end |
||||||||||||
|
||||||||||||
""" | ||||||||||||
OncePerTask{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(); | ||||||||||||
Making lazy task value...done. | ||||||||||||
|
||||||||||||
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.