-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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 fiber safety to __crystal_once
& class_[getter|property]?(&)
macros
#15340
base: master
Are you sure you want to change the base?
Changes from all commits
060c6cf
86ff3c4
f08ef93
dc635fb
ab8b994
5728135
cabbc9c
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 |
---|---|---|
|
@@ -7,12 +7,11 @@ | |
# - `__crystal_once`: called each time a constant or class variable has to be | ||
# initialized and is its responsibility to verify the initializer is executed | ||
# only once and to fail on recursion. | ||
|
||
# In multithread mode a mutex is used to avoid race conditions between threads. | ||
# | ||
# On Win32, `Crystal::System::FileDescriptor#@@reader_thread` spawns a new | ||
# thread even without the `preview_mt` flag, and the thread can also reference | ||
# Crystal constants, leading to race conditions, so we always enable the mutex. | ||
# Also defines the `Crystal.once(flag, &)` method used to protect lazy | ||
# initialization of class getters & properties. | ||
# | ||
# A `Mutex` is used to avoid race conditions between threads and fibers. | ||
|
||
{% if compare_versions(Crystal::VERSION, "1.16.0-dev") >= 0 %} | ||
# This implementation uses an enum over the initialization flag pointer for | ||
|
@@ -22,28 +21,32 @@ | |
# :nodoc: | ||
enum OnceState : Int8 | ||
Processing = -1 | ||
Uninitialized = 0 | ||
Initialized = 1 | ||
Uninitialized = 0 | ||
Initialized = 1 | ||
end | ||
|
||
{% if flag?(:preview_mt) || flag?(:win32) %} | ||
@@once_mutex = uninitialized Mutex | ||
@@once_mutex = uninitialized Mutex | ||
|
||
# :nodoc: | ||
def self.once_mutex=(@@once_mutex : Mutex) | ||
end | ||
{% end %} | ||
# :nodoc: | ||
def self.once_mutex=(@@once_mutex : Mutex) | ||
end | ||
|
||
# :nodoc: | ||
# | ||
# Identical to `__crystal_once` but takes a block with possibly closured | ||
# data. Used by `class_[getter|property](declaration, &block)` for example. | ||
def self.once(flag : OnceState*, &) : Nil | ||
return if flag.value.initialized? | ||
once_exec(flag) { yield } | ||
end | ||
|
||
# :nodoc: | ||
# | ||
# Using @[NoInline] so LLVM optimizes for the hot path (var already | ||
# initialized). | ||
@[NoInline] | ||
def self.once(flag : OnceState*, initializer : Void*) : Nil | ||
{% if flag?(:preview_mt) || flag?(:win32) %} | ||
@@once_mutex.synchronize { once_exec(flag, initializer) } | ||
{% else %} | ||
once_exec(flag, initializer) | ||
{% end %} | ||
def self.once(flag : OnceState*, initializer : Void*, closure_data : Void*) : Nil | ||
once_exec(flag) { Proc(Nil).new(initializer, closure_data).call } | ||
|
||
# safety check, and allows to safely call `Intrinsics.unreachable` in | ||
# `__crystal_once` | ||
|
@@ -53,25 +56,27 @@ | |
end | ||
end | ||
|
||
private def self.once_exec(flag : OnceState*, initializer : Void*) : Nil | ||
case flag.value | ||
in .initialized? | ||
return | ||
in .uninitialized? | ||
flag.value = :processing | ||
Proc(Nil).new(initializer, Pointer(Void).null).call | ||
flag.value = :initialized | ||
in .processing? | ||
raise "Recursion while initializing class variables and/or constants" | ||
private def self.once_exec(flag, &) | ||
@@once_mutex.synchronize do | ||
case flag.value | ||
in .initialized? | ||
return | ||
in .uninitialized? | ||
flag.value = OnceState::Processing | ||
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. issue: Shouldn't we unlock the mutex at this point? The initialization code might be expensive and delay execution of other initializers. I figure it ought to be possible to execute different initializers concurrently. By setting the flag value to 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. We need the mutex reentrancy for thread A to notice the recursion but also to block thread B from accessing the value until thread A has properly initialized it. Otherwise thread B could lock the mutex, notice that the flag is :processing and fail with a recursion issue (oops) instead of waiting. To solve this, we'd need a checked mutex (not a reentrant one) per constant and class variable, which might not be a bad idea 🤔 |
||
yield | ||
flag.value = OnceState::Initialized | ||
in .processing? | ||
raise "Recursion while initializing class variables and/or constants" | ||
end | ||
end | ||
end | ||
end | ||
|
||
# :nodoc: | ||
fun __crystal_once_init : Nil | ||
{% if flag?(:preview_mt) || flag?(:win32) %} | ||
Crystal.once_mutex = Mutex.new(:reentrant) | ||
{% end %} | ||
Thread.init | ||
Fiber.init | ||
Crystal.once_mutex = Mutex.new(:reentrant) | ||
end | ||
|
||
# :nodoc: | ||
|
@@ -83,7 +88,7 @@ | |
fun __crystal_once(flag : Crystal::OnceState*, initializer : Void*) : Nil | ||
return if flag.value.initialized? | ||
|
||
Crystal.once(flag, initializer) | ||
Crystal.once(flag, initializer, Pointer(Void).null) | ||
|
||
# tell LLVM that it can optimize away repeated `__crystal_once` calls for | ||
# this global (e.g. repeated access to constant in a single funtion); | ||
|
@@ -94,49 +99,72 @@ | |
# This implementation uses a global array to store the initialization flag | ||
# pointers for each value to find infinite loops and raise an error. | ||
|
||
# :nodoc: | ||
class Crystal::OnceState | ||
@rec = [] of Bool* | ||
|
||
@[NoInline] | ||
def once(flag : Bool*, initializer : Void*) | ||
unless flag.value | ||
if @rec.includes?(flag) | ||
raise "Recursion while initializing class variables and/or constants" | ||
end | ||
@rec << flag | ||
module Crystal | ||
# :nodoc: | ||
class OnceState | ||
@mutex = Mutex.new(:reentrant) | ||
@rec = [] of Bool* | ||
|
||
Proc(Nil).new(initializer, Pointer(Void).null).call | ||
flag.value = true | ||
def once(flag : Bool*, &) | ||
return if flag.value | ||
once_exec(flag) { yield } | ||
end | ||
|
||
@rec.pop | ||
@[NoInline] | ||
def once(flag : Bool*, initializer : Void*, closure_data : Void*) | ||
once_exec(flag) { Proc(Nil).new(initializer, closure_data).call } | ||
end | ||
end | ||
|
||
{% if flag?(:preview_mt) || flag?(:win32) %} | ||
@mutex = Mutex.new(:reentrant) | ||
private def once_exec(flag, &) | ||
@mutex.synchronize do | ||
return if flag.value | ||
|
||
@[NoInline] | ||
def once(flag : Bool*, initializer : Void*) | ||
unless flag.value | ||
@mutex.synchronize do | ||
previous_def | ||
if @rec.includes?(flag) | ||
raise "Recursion while initializing class variables and/or constants" | ||
end | ||
@rec << flag | ||
|
||
yield | ||
flag.value = true | ||
|
||
@rec.pop | ||
end | ||
end | ||
{% end %} | ||
end | ||
|
||
@@once_state = uninitialized OnceState | ||
|
||
# :nodoc: | ||
def self.once_state=(@@once_state : OnceState) | ||
end | ||
|
||
# :nodoc: | ||
def self.once(flag : Bool*, &) : Nil | ||
return if flag.value | ||
@@once_state.once(flag) { yield } | ||
end | ||
end | ||
|
||
# :nodoc: | ||
fun __crystal_once_init : Void* | ||
Crystal::OnceState.new.as(Void*) | ||
Thread.init | ||
Fiber.init | ||
(Crystal.once_state = Crystal::OnceState.new).as(Void*) | ||
end | ||
|
||
# :nodoc: | ||
@[AlwaysInline] | ||
fun __crystal_once(state : Void*, flag : Bool*, initializer : Void*) | ||
return if flag.value | ||
state.as(Crystal::OnceState).once(flag, initializer) | ||
state.as(Crystal::OnceState).once(flag, initializer, Pointer(Void).null) | ||
Intrinsics.unreachable unless flag.value | ||
end | ||
{% end %} | ||
|
||
{% if flag?(:interpreted) %} | ||
# make sure to initialize the mutex so we can use Crystal.once in the | ||
# class_[getter|property]? macros; the compiler does the call by itself, but | ||
# the interpreter doesn't (it doesn't use __crystal_once to protect the | ||
# initialization of constants and class vars). | ||
__crystal_once_init | ||
{% end %} |
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.
question: Why pass the function pointer and closure data as separate values instead of a
Proc
instance?