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 |
Comments