Skip to content

Ensuring class_getter runs exactly once under concurrent access #14905

Open
@HertzDevil

Description

A class_getter with a block usually runs exactly once:

module Foo
  class_getter(x : Int32) { 1 }

  # same as:
  def self.x : Int32
    if (x = @@x).nil?
      @@x = 1
    else
      x
    end
  end
end

However, if the current fiber is ever suspended, the block might be run multiple times:

module Foo
  class_getter x : Int32 do
    puts "Foo.x" # this runs 5 times
    Fiber.yield
    1
  end
end

ch = Channel(Int32).new
5.times { spawn { ch.send Foo.x } }
5.times { ch.receive }

And if -Dpreview_mt is in effect, the block also gets run more than once, even without the Fiber.yield.

Often, Foo.x represents some kind of cached object that might be expensive to compute, like #14891 and the Unicode data tables; if we also disregard the block's return value, then this would include the various interrupt handlers guarded by Atomic::Flags too. There should be an easier way in the standard library to ensure this block is really run exactly once, regardless of the degree of concurrent access.

Constants are one alternative, but whether they run at program startup or on first access is rather opaque, and also they are slightly broken, such as in #13054.

This could apply to class_property as well, and less likely to the instance variants getter and property.

Metadata

Assignees

Type

No type

Projects

  • Status

    In Progress

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions