Skip to main content
โšก Calmops

Using Class Variables for Counters and Shared State in Ruby

Introduction

One of the most common uses of class variables (@@var) is tracking shared state across all instances of a class โ€” like counting how many objects have been created. This article shows the pattern, its pitfalls, and thread-safe alternatives.

Basic Counter Pattern

class HelloCount
  @@count = 0

  def self.count
    @@count
  end

  def initialize(myname = "Ruby")
    @name = myname
  end

  def hello
    @@count += 1
    puts "Hello, world, I am #{@name}"
  end
end

bob   = HelloCount.new("Bob")
alice = HelloCount.new("Alice")
ruby  = HelloCount.new

p HelloCount.count  # => 0

bob.hello    # => Hello, world, I am Bob
alice.hello  # => Hello, world, I am Alice
ruby.hello   # => Hello, world, I am Ruby

p HelloCount.count  # => 3

@@count is shared across all instances. Every call to hello increments the same counter regardless of which instance calls it.

Counting Object Creation

A common variant counts how many instances have been created:

class Connection
  @@total_created = 0
  @@active_count  = 0

  def self.total_created; @@total_created; end
  def self.active_count;  @@active_count;  end

  def initialize(host)
    @host = host
    @@total_created += 1
    @@active_count  += 1
    puts "Connected to #{@host} (active: #{@@active_count})"
  end

  def close
    @@active_count -= 1
    puts "Closed #{@host} (active: #{@@active_count})"
  end
end

c1 = Connection.new("db1.example.com")  # active: 1
c2 = Connection.new("db2.example.com")  # active: 2
c3 = Connection.new("cache.example.com") # active: 3

c1.close  # active: 2

puts "Total created: #{Connection.total_created}"  # => 3
puts "Currently active: #{Connection.active_count}" # => 2

The Inheritance Problem

Class variables are shared across the entire inheritance hierarchy โ€” a subclass modifying @@count affects the parent class too:

class Animal
  @@count = 0
  def initialize; @@count += 1; end
  def self.count; @@count; end
end

class Dog < Animal; end
class Cat < Animal; end

Dog.new
Dog.new
Cat.new

puts Animal.count  # => 3 โ€” all share the same @@count
puts Dog.count     # => 3 โ€” same variable!
puts Cat.count     # => 3 โ€” same variable!

If you want per-class counts, use class instance variables instead:

class Animal
  @count = 0

  def self.count; @count ||= 0; end

  def self.inherited(subclass)
    subclass.instance_variable_set(:@count, 0)
  end

  def initialize
    self.class.instance_variable_set(
      :@count,
      self.class.instance_variable_get(:@count) + 1
    )
  end
end

class Dog < Animal; end
class Cat < Animal; end

Dog.new; Dog.new
Cat.new

puts Animal.count  # => 0
puts Dog.count     # => 2
puts Cat.count     # => 1

Thread Safety Warning

Class variables are not thread-safe. The += operation is not atomic โ€” it’s a read-modify-write sequence that can produce race conditions in multi-threaded code:

# UNSAFE in multi-threaded environment
class Counter
  @@count = 0
  def self.increment; @@count += 1; end  # race condition!
  def self.count; @@count; end
end

# Simulate race condition
threads = 1000.times.map { Thread.new { Counter.increment } }
threads.each(&:join)
puts Counter.count  # might not be 1000!

Thread-Safe Counter with Mutex

class ThreadSafeCounter
  @@count = 0
  @@mutex = Mutex.new

  def self.increment
    @@mutex.synchronize { @@count += 1 }
  end

  def self.decrement
    @@mutex.synchronize { @@count -= 1 }
  end

  def self.count
    @@mutex.synchronize { @@count }
  end

  def self.reset
    @@mutex.synchronize { @@count = 0 }
  end
end

threads = 1000.times.map { Thread.new { ThreadSafeCounter.increment } }
threads.each(&:join)
puts ThreadSafeCounter.count  # => 1000 (always correct)

Using Concurrent::AtomicFixnum (concurrent-ruby gem)

For production code, the concurrent-ruby gem provides lock-free atomic counters:

require 'concurrent'

class RequestCounter
  @@total    = Concurrent::AtomicFixnum.new(0)
  @@errors   = Concurrent::AtomicFixnum.new(0)
  @@successes = Concurrent::AtomicFixnum.new(0)

  def self.record_request(success:)
    @@total.increment
    success ? @@successes.increment : @@errors.increment
  end

  def self.stats
    {
      total:    @@total.value,
      errors:   @@errors.value,
      successes: @@successes.value,
      error_rate: @@errors.value.to_f / [@@total.value, 1].max
    }
  end
end

# Safe to call from multiple threads
RequestCounter.record_request(success: true)
RequestCounter.record_request(success: false)
puts RequestCounter.stats.inspect

Better Alternative: Class Instance Variable Counter

For most use cases, a class instance variable with a Mutex is cleaner than a class variable:

class EventTracker
  @events = []
  @mutex  = Mutex.new

  class << self
    def track(event)
      @mutex.synchronize { @events << { event: event, time: Time.now } }
    end

    def count
      @mutex.synchronize { @events.size }
    end

    def recent(n = 10)
      @mutex.synchronize { @events.last(n) }
    end

    def reset
      @mutex.synchronize { @events.clear }
    end
  end
end

EventTracker.track("user_login")
EventTracker.track("page_view")
EventTracker.track("purchase")

puts EventTracker.count   # => 3
puts EventTracker.recent(2).map { |e| e[:event] }.inspect
# => ["page_view", "purchase"]

Practical: Rate Limiter

A real-world use of class-level counters โ€” a simple rate limiter:

class RateLimiter
  @@requests = Hash.new { |h, k| h[k] = [] }
  @@mutex    = Mutex.new

  WINDOW_SECONDS = 60
  MAX_REQUESTS   = 100

  def self.allow?(identifier)
    @@mutex.synchronize do
      now = Time.now.to_f
      cutoff = now - WINDOW_SECONDS

      # Remove old requests outside the window
      @@requests[identifier].reject! { |t| t < cutoff }

      if @@requests[identifier].size < MAX_REQUESTS
        @@requests[identifier] << now
        true
      else
        false
      end
    end
  end

  def self.remaining(identifier)
    @@mutex.synchronize do
      now = Time.now.to_f
      cutoff = now - WINDOW_SECONDS
      @@requests[identifier].reject! { |t| t < cutoff }
      [MAX_REQUESTS - @@requests[identifier].size, 0].max
    end
  end
end

puts RateLimiter.allow?("user_42")    # => true
puts RateLimiter.remaining("user_42") # => 99

Summary

Pattern Thread Safe Per-Class Use When
@@count += 1 No No (shared) Single-threaded, simple scripts
@@count + Mutex Yes No (shared) Multi-threaded, shared counter
@count (class instance var) No Yes Per-class tracking
@count + Mutex Yes Yes Multi-threaded, per-class
Concurrent::AtomicFixnum Yes Configurable Production, high-concurrency

Resources

Comments