Project

General

Profile

Actions

Feature #20875

open

Atomic initialization for Ractor local storage

Added by ko1 (Koichi Sasada) 27 days ago. Updated 1 day ago.

Status:
Open
Assignee:
-
Target version:
-
[ruby-core:119769]

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

https://github.com/ruby/ruby/pull/12014

Actions #1

Updated by ko1 (Koichi Sasada) 27 days ago

  • Description updated (diff)
Actions #2

Updated by ko1 (Koichi Sasada) 27 days ago

  • Description updated (diff)
Actions #3

Updated by ko1 (Koichi Sasada) 27 days ago

  • Description updated (diff)
Actions #4

Updated by ko1 (Koichi Sasada) 27 days ago

  • Description updated (diff)

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.

Actions

Also available in: Atom PDF

Like0
Like0Like0Like0Like0Like0Like0