Module: ThreadLocalVarAccessors

Defined in:
lib/thread_local_var_accessors.rb,
lib/thread_local_var_accessors/version.rb

Overview

This module has methods making it easy to use the Concurrent::ThreadLocalVar as instance variables. This makes instance variables using this code as actually thread-local, without also leaking memory over time.

See [Why Concurrent::ThreadLocalVar](github.com/ruby-concurrency/concurrent-ruby/blob/master/lib/concurrent-ruby/concurrent/atomic/thread_local_var.rb#L10-L17) to understand why we use TLVs instead of ‘Thread.current.thread_variable_(set|get)`

Class Methods

The following class methods declare ‘Concurrent::ThreadLocalVar` reader, writer, and accessors with these class methods:

tlv_reader :var1, :var2, ...
tlv_writer :var3, :var4, ...
tlv_accessor :var5, :var6, ...
  • ‘tlv_reader` creates a method with the name `name`, that references the instance variable names ’@name’, which is expected to be either nil, or already have a ‘Concurrent::ThreadLocalVar` instance.

  • ‘tlv_writer` creates a method with the name `name=`, which accepts a single argument that is the new value. This method checks for an existing value on the instance variable named “@name”, which should be a `Concurrent::ThreadLocalVar` instance. If `@name` value is nil, then a new `Concurrent::ThreadLocalVar` instance is assigned to it. In either case, the instance variable’s TLV object is assigned the new value, which is returned.

  • ‘tlv_accessor` - creates both a `tlv_reader` and a `tlv_writer`.

Just as with ‘attr_accessor` methods, obtaining values and setting values becomes very simple:

tlv_accessor :timeout

Instance Methods

Create a new TLV instance with an associated default, applied across all threads.

tlv_new :timeout, default_timeout
tlv_new :timeout { default_timeout }

This method always assignes the instance variable. It is equivalent to:

@instance_var = ThreadLocalVar.new(default)
@instance_var = ThreadLocalVar.new { default }

Assign the current thread value for the TLV variable:

self.timeout = 0.5  # stores the TLV value just for this thread

which is equivalent to:

@timeout ||= ThreadLocalVar.new
@timeout.value = 0.5

Reference the current thread value for the TLV variable:

timeout  # fetches the current TLV value, unique to each thread

which is the same as:

@timeout ||= ThreadLocalVar.new
@timeout.value

Alternative ways to initialize the thread-local value:

tlv_set(:timeout, 0)
tlv_set(:timeout) # ensure that @timeout is initialized to an LTV
tlv_set_once(:timeout, value) # set timeout only if it isn't already set

Each thread-local instance can be independently assigned a value, which defaults to the default value, or block, that was associated with the original ‘ThreadLocalVar.new` method. This module also provides an easy way to do this.

Initializes a TLV on the ‘@timeout` instance variable with a default value of 0.15 seconds:

tlv_new(:timeout, 0.15)

This does the same, but uses a block (a Proc object) to possibly return a dynamic default value, as the proc is invoked each time the TLV instance is evaluted in a Thread.

tlv_new(:sleep_time) { computed_sleep_time }

The block-proc is evaluated at the time the default value is needed, not when the TLV is assigned to the instance variable. In other words, much later during process, when the instance variable value is evaluated, that is when the default block is evaluated.

The ‘tlv_new` method always assigns a new TLVar to the named instance variable.

There is a corresponding method to either create a new TLVar instance or assign the default value to the existing TLVar instance: ‘tlv_init`.

tlv_init(IVAR, DEFAULT)
tlv_init(IVAR) { DEFAULT }

Note that ‘tlv_init` does not assign the thread-local value; it assigns the _instance variable_ to a new TLV with the given default. If any thread evaluates that instance variable, the default value will be returned unless and until the current thread associates a new, thread-local value with the TLV instance.

The purpose of ‘tlv_init` is to make the initialization of a TLVar be idempotent:

  1. if the TLVar does not yet exist, it is created with the default value.

  2. if the TLVar already exists, it’s default value is set.

In neither case are any thread-local value set; only the default.

The default for an existing TLV can be redefined, using either an optional default value, or an optional default block.

tlv_set_default(:timeout, new_default)
tlv_set_default(:timeout) { new_default }

The default for an existing TLV can also be obtained, independently of the current thread’s local value, if any:

tlv_default(:timeout)

The following methods are used within the above reader, writer, accessor methods:

tlv_get(name)        - fetches the value of TLV `name`
tlv_set(name, value) - stores the value into the TLV `name`

There is a block form to ‘tls_set`:

tlv_set(name) { |old_val| new_val }

The ‘name` argument to `tlv_get` and `tlv_set` is the same as given on the accessor, reader, writer methods: either a string or symbol name, automatically converted as needed to instance-variable syntax (eg: :@name), and setter-method name syntax (eg :name=).

Example:

tlv_accessor :timeout

Creates reader and writer methods called “timeout”, and “timeout=”, respectively. Both of these methods interrogate the instance variable “@timeout”, which is initialized (by ‘tlv_set`) to contain a `Concurrent::ThreadLocalVar.new` value.

The writer methods support using attached blocks to receive the current value, if any, and should return the value to be stored.

The ‘timeout` reader method would look like this:

def timeout
  instance_variable_get(:@timeout)&.value
end

The ‘timeout=’ writer method would look like this:

def timeout=(value)
  var = instance_variable_get(:@timeout) ||
        instance_variable_set(:@timeout, Concurrent::ThreadLocalVar.new)
  var.value = block_given? ? yield(var.value) : value
end

Each thread referencing the instance variable, will get the same TLV object, but when the ‘.value` method is invoked, each thread will receive the initial value, or whatever local value may have been assigned subsequently, or the default, which is the same across all the threads.

To obtain the value of such an TLV instance variable, do:

@timeout.value

To assign a new value to an TLV instance:

@timeout.value = new_value

Defined Under Namespace

Modules: ClassMethods

Constant Summary collapse

RELEASES =
[
  ['1.4.0', '2024-07-24'],
  ['1.3.1', '2024-05-09'],
  ['1.3.0', '2023-05-04']
].freeze
VERSION =

Instance Method Summary collapse

Instance Method Details

#tlv_default(name) ⇒ Object

Fetches the default value for the TLVar at the ivar


300
301
302
# File 'lib/thread_local_var_accessors.rb', line 300

def tlv_default(name)
  tlv_get_default(tlv_get_var(name))
end

#tlv_get(name) ⇒ Object

instance methods Returns the value of the TLV instance variable, if any.


232
233
234
# File 'lib/thread_local_var_accessors.rb', line 232

def tlv_get(name)
  tlv_get_var(name)&.value
end

#tlv_get_default(tlv) ⇒ Object

gets the default value from a TLV this masks the private ThreadLocalVar#default method


306
307
308
# File 'lib/thread_local_var_accessors.rb', line 306

def tlv_get_default(tlv)
  tlv&.send(:default)
end

#tlv_get_var(name) ⇒ ThreadLocalVar

Returns the TLV, if any, of the named instance variable.

Returns:

  • (ThreadLocalVar)

    the TLV, if any, of the named instance variable.


237
238
239
240
# File 'lib/thread_local_var_accessors.rb', line 237

def tlv_get_var(name)
  var = instance_variable_get(name.to_ivar)
  var if var.is_a?(Concurrent::ThreadLocalVar)
end

#tlv_init(name, default = nil, &block) ⇒ Object

Creates a new TLVar with a default, or assigns the default value to an existing TLVar, without affecting any existing thread-local values. Returns the value of the new TLVar. Equivalent to:

@name ||= ThreadLocalVar.new
@name.instance_variable_set(:@default, block_given? ? yield : default)
@name.value

294
295
296
297
# File 'lib/thread_local_var_accessors.rb', line 294

def tlv_init(name, default = nil, &block)
  tlv_set_default(name, default, &block)
  tlv_get(name)
end

#tlv_new(name, default = nil, &block) ⇒ Object

Creates a new TLVar with the given default value or block. Equivalent to:

@name = ThreadLocalVar.new(block_given? ? yield : default)

280
281
282
283
284
285
# File 'lib/thread_local_var_accessors.rb', line 280

def tlv_new(name, default = nil, &block)
  instance_variable_set(
    name.to_ivar,
    Concurrent::ThreadLocalVar.new(default, &block)
  )
end

#tlv_set(name, value = nil, &block) ⇒ Object

If the instance variable is already a TLV, then set it’s value to the given value, or the value returned by the block, if any. If the instance variable is not a TLV, then create a new TLV, initializing it’s default value with the given value, or the value returned by the block, if any, then, returning it’s local value – which returns the default value.

This method is equivalent to:

if @name.is_a?(ThreadLocalVar)
  @name.value = block_given? ? yield : value
else
  @name = ThreadLocalVar(value, &block)
end

Returns:

  • (Object)

    the value of the TLV instance variable


255
256
257
258
259
260
261
# File 'lib/thread_local_var_accessors.rb', line 255

def tlv_set(name, value = nil, &block)
  if (var = tlv_get_var(name))
    tlv_set_var(var, value, &block)
  else
    tlv_new(name, value, &block).value
  end
end

#tlv_set_default(name, default = nil, &block) ⇒ Object

Sets the default value or block for the TLV _(which is applied across all threads)_. Creates a new TLV if the instance variable is not initialized.

Returns:

  • (Object)

    the effective default value of the TLV instance variable


313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/thread_local_var_accessors.rb', line 313

def tlv_set_default(name, default = nil, &block)
  tlv = instance_variable_get(name.to_ivar)
  if tlv
    raise ArgumentError, 'tlv_set_default: can only use a default or a block, not both' if default && block

    # in both cases, the default is set to the given value, only one of which
    # can be given at a time.
    tlv.instance_variable_set(:@default_block, block)
    tlv.instance_variable_set(:@default, default)
    tlv
  else
    tlv = tlv_new(name, default, &block)
  end
  tlv_get_default(tlv)
end

#tlv_set_once(name, value = nil, &block) ⇒ Object

Sets the thread-local value of the TLV instance variable, but only if it is not already set. It is equivalent to:

@name ||= ThreadLocalVar.new
@name.value ||= block_given? yield : value

267
268
269
270
271
272
273
274
275
# File 'lib/thread_local_var_accessors.rb', line 267

def tlv_set_once(name, value = nil, &block)
  if (var = tlv_get_var(name))&.value
    var.value
  elsif var # var is set, but its value is nil
    tlv_set_var(var, value, &block)
  else # var is not set, initialize it
    tlv_new(name, value, &block).value
  end
end