Feature #20875
openAtomic initialization for Ractor local storage
Description
Motivation¶
Now there is no way to initialize Ractor local storage in multi-thread.
For example, if we want to introduce per-Ractor counter, which should be protected with a per-Ractor Mutex for multi-threading support.
def init
Ractor[:cnt] = 0
Ractor[:mtx] = Mutex.new
end
def inc
init unless Ractor[:cnt]
Ractor[:mtx].synchronize do
Ractor[:cnt] += 1
end
end
In this code, if inc
was called on multiple threads, init
can be called with multiple threads and cnt
can not be synchronized correctly.
Proposal¶
Let's introduce Ractor.local_storage_init(sym){ block }
to initialize values in Ractor local storage.
If there is no slot for sym
, synchronize with per-Ractor mutex and call block
and the slot will be filled with the evaluation with the block result. The return value of this method will be the filled value.
Otherwise, returning corresponding value will be returned.
The implementation is like that (in C):
class Ractor
def self.local_storage_init(sym)
Ractor.per_ractor_mutex.synchronize do
if Ractor.local_storage_has_key?(sym)
Ractor[:sym]
else
Ractor[:sym] = yield
end
end
end
end
The above examples will be rewritten with the following code:
def inc
Ractor.local_storage_init(:mtx) do
Ractor[:cnt] = 0
Mutex.new
end.synchronize do
Ractor[:cnt] += 1
end
end
Discussion¶
Approach¶
There is another approach like pthread_atfork
, maybe like Ractor.atcreate{ init }
. A library registers a callback which will be called when a new ractor is created.
However, there are many Ractors which don't use the library, so that atcreate
can be huge overhead for Ractor creation.
Naming¶
I propose local_storage_init
, but not sure it matches.
I also proposed Ractor.local_variable_init(sym)
, but Matz said he doesn't like this naming because it should not be a "variable".
(there is a Thread#thread_variable_get
method, though).
On another aspect, lcoal_storage_init
seems it clears all of ractor local storage slots.
Reentrancy¶
This proposal uses Mutex
, so it is not reentrant. I believe it should be simple and using Monitor is too much.
(but it is not big issue, though)
Implementation¶
Updated by ko1 (Koichi Sasada) 5 days ago ยท Edited
@matz (Yukihiro Matsumoto) how about Ractor.local_storage_once(key){ ... }
? It is from pthread_once.
Ractor.once(key){ ... }
seems too short?
def inc
Ractor.local_storage_once(:mtx) do
Ractor[:cnt] = 0
Mutex.new
end.synchronize do
Ractor[:cnt] += 1
end
end
or
def inc
Ractor.once(:mtx) do
Ractor[:cnt] = 0
Mutex.new
end.synchronize do
Ractor[:cnt] += 1
end
end
Updated by Dan0042 (Daniel DeLorme) 1 day ago
Would it be possible to make Ractor[:mtx] ||= Mutex.new
behave in an atomic way? Like maybe add a special []||=
method which is automatically called such that Ractor[:mtx] ||= Mutex.new
becomes equivalent to Ractor[:mtx] || Ractor.send(:"[]||=", :mtx, Mutex.new)
I'm just throwing out the general idea here, because it would be nice to use common ruby idioms instead of yet another special API to handle concurrent behavior.