CrackedRuby logo

CrackedRuby

Thread Local Variables

Thread local variables in Ruby provide isolated storage for data that needs to be accessible within a single thread without affecting other threads.

Concurrency and Parallelism Threading
6.1.3

Overview

Thread local variables in Ruby create separate storage spaces for data that exists independently within each thread's execution context. Ruby implements thread local variables through the Thread class, which provides methods for setting, getting, and managing thread-specific data. Each thread maintains its own hash-like storage where variables can be stored and retrieved using string or symbol keys.

The Thread.current method returns a reference to the currently executing thread, and thread objects support hash-like operations for storing data. When a thread terminates, its thread local storage gets automatically cleaned up by the garbage collector. Thread local variables solve the problem of sharing state between methods within a single thread without using global variables or passing parameters through multiple method calls.

Ruby's thread local implementation uses a hash table internal to each thread object. The storage supports any Ruby object as a value and uses string or symbol keys for identification. Thread local variables remain accessible throughout the entire lifecycle of a thread, from creation until termination.

# Basic thread local variable assignment
Thread.current[:user_id] = 12345
Thread.current["request_id"] = "abc-123"

# Retrieving thread local variables
user_id = Thread.current[:user_id]
request_id = Thread.current["request_id"]
puts "User: #{user_id}, Request: #{request_id}"
# => User: 12345, Request: abc-123

Thread local variables provide isolation between concurrent threads. When multiple threads execute simultaneously, each thread's local variables remain separate and modifications in one thread do not affect variables in other threads. This isolation makes thread local variables useful for storing request-specific data, database connections, and other context information that should remain thread-specific.

# Demonstrating thread isolation
3.times do |i|
  Thread.new do
    Thread.current[:thread_num] = i
    sleep 0.1
    puts "Thread #{Thread.current[:thread_num]} finished"
  end
end

# Each thread prints its own number, showing isolation

Basic Usage

Thread local variables in Ruby use the Thread.current object as a hash-like container. The Thread.current[] method retrieves values, while Thread.current[]= stores values. Keys can be strings, symbols, or any hashable object, though strings and symbols are most common.

# Setting thread local variables
Thread.current[:database_connection] = Database.connect
Thread.current["current_user"] = User.find(session[:user_id])
Thread.current[:start_time] = Time.now

# Reading thread local variables
db = Thread.current[:database_connection]
user = Thread.current["current_user"]
elapsed = Time.now - Thread.current[:start_time]

The Thread#[] and Thread#[]= methods work identically to hash access. Ruby treats thread local storage as a hash table where each key maps to a value. Missing keys return nil rather than raising exceptions, matching standard hash behavior.

# Checking for existence and providing defaults
if Thread.current[:logger]
  Thread.current[:logger].info("Processing request")
else
  Thread.current[:logger] = Logger.new(STDOUT)
end

# Using fetch with defaults
logger = Thread.current.fetch(:logger) { Logger.new(STDOUT) }
timeout = Thread.current.fetch(:timeout, 30)

Thread local variables commonly store context information that needs to be accessible throughout a request or operation. Web applications frequently use thread local variables to store request-specific data like user authentication, request identifiers, and database connections.

class RequestContext
  def self.current_user
    Thread.current[:current_user]
  end

  def self.current_user=(user)
    Thread.current[:current_user] = user
  end

  def self.request_id
    Thread.current[:request_id] ||= SecureRandom.uuid
  end

  def self.clear!
    Thread.current[:current_user] = nil
    Thread.current[:request_id] = nil
  end
end

# Usage in request handling
def handle_request(request)
  RequestContext.current_user = authenticate(request)
  RequestContext.request_id  # Generates UUID on first access
  
  process_request(request)
ensure
  RequestContext.clear!
end

The Thread#keys method returns an array of all keys currently stored in the thread's local storage. The Thread#key? method checks whether a specific key exists. These methods help inspect and manage thread local state.

# Inspecting thread local state
Thread.current[:a] = 1
Thread.current[:b] = 2
Thread.current["c"] = 3

puts Thread.current.keys.inspect
# => [:a, :b, "c"]

puts Thread.current.key?(:a)
# => true

puts Thread.current.key?(:missing)
# => false

Thread Safety & Concurrency

Thread local variables provide inherent thread safety by design since each thread maintains completely separate storage. Operations on thread local variables within a single thread require no synchronization because no other thread can access the same storage space. However, the objects stored as values in thread local variables may themselves require thread safety considerations if they are shared between threads.

# Thread local variables are inherently thread-safe
threads = 10.times.map do |i|
  Thread.new do
    Thread.current[:counter] = 0
    1000.times { Thread.current[:counter] += 1 }
    Thread.current[:counter]
  end
end

results = threads.map(&:value)
puts results.inspect
# => [1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000]
# Each thread safely increments its own counter

When thread local variables store references to shared mutable objects, those objects still require proper synchronization. Thread local storage provides isolation for the reference itself, but not for the object being referenced if multiple threads have access to the same object instance.

# Shared object referenced in thread local storage
shared_array = []
mutex = Mutex.new

5.times do |i|
  Thread.new do
    Thread.current[:shared_ref] = shared_array
    Thread.current[:thread_id] = i
    
    10.times do
      mutex.synchronize do
        Thread.current[:shared_ref] << Thread.current[:thread_id]
      end
    end
  end
end.each(&:join)

puts shared_array.size
# => 50 (all threads safely added to shared array)

Thread local variables work well with Ruby's threading primitives like Mutex, ConditionVariable, and Queue. Thread-specific state can be stored in thread local variables while using these primitives to coordinate between threads when necessary.

# Coordinating threads while maintaining thread local state
queue = Queue.new
results = {}
mutex = Mutex.new

workers = 3.times.map do |worker_id|
  Thread.new do
    Thread.current[:worker_id] = worker_id
    Thread.current[:processed] = 0
    
    while (item = queue.pop(true) rescue nil)
      # Process item using thread local state
      result = "Worker #{Thread.current[:worker_id]} processed #{item}"
      Thread.current[:processed] += 1
      
      mutex.synchronize do
        results[Thread.current[:worker_id]] = Thread.current[:processed]
      end
    end
  end
end

# Add work items
10.times { |i| queue << "item_#{i}" }
workers.each(&:join)

puts results.inspect
# => {0=>4, 1=>3, 2=>3} (workers processed different amounts)

Race conditions cannot occur when accessing thread local variables from within the owning thread, but they can still occur if thread references are passed between threads and accessed concurrently. Ruby's Thread objects themselves are not thread-safe for concurrent modification of their local storage from multiple threads.

# Unsafe: accessing another thread's local storage
main_thread = Thread.current
Thread.current[:data] = "main thread data"

worker = Thread.new do
  # This creates a race condition - don't do this
  main_thread[:data] = "modified by worker"
end

worker.join
puts Thread.current[:data]
# => May print either value due to race condition

Common Pitfalls

Memory leaks represent a significant pitfall with thread local variables. Objects stored in thread local variables remain referenced as long as the thread exists, preventing garbage collection. Long-running threads that accumulate thread local data without cleanup can consume substantial memory.

# Memory leak example - don't do this
class BadService
  def self.process_request(request)
    # This accumulates without ever being cleaned up
    Thread.current[:requests] ||= []
    Thread.current[:requests] << request
    
    # Process request
    handle_request(request)
  end
end

# Each request adds to thread local storage indefinitely
# In a web server, this creates a memory leak

Thread pools compound memory leak problems because threads are reused across multiple operations. Thread local variables set during one operation remain available during subsequent operations unless explicitly cleared. This can cause data from previous operations to leak into current operations.

# Thread pool memory leak
require 'concurrent'

pool = Concurrent::ThreadPoolExecutor.new(min_threads: 2, max_threads: 5)

# First task sets thread local variable
pool.post do
  Thread.current[:sensitive_data] = "secret key 12345"
  puts "Task 1: #{Thread.current[:sensitive_data]}"
end

sleep 0.1

# Second task on same thread sees previous data
pool.post do
  puts "Task 2: #{Thread.current[:sensitive_data]}"
  # => "secret key 12345" - data leaked from previous task!
end

pool.shutdown
pool.wait_for_termination

Proper cleanup requires explicit clearing of thread local variables, especially in thread pool environments. Web frameworks and libraries should clear thread local state between requests to prevent data leakage.

# Proper cleanup pattern
class SafeRequestHandler
  def self.handle_request(request)
    setup_context(request)
    process_request(request)
  ensure
    cleanup_context
  end

  private

  def self.setup_context(request)
    Thread.current[:request_id] = request.id
    Thread.current[:user_id] = request.user_id
    Thread.current[:start_time] = Time.now
  end

  def self.cleanup_context
    Thread.current[:request_id] = nil
    Thread.current[:user_id] = nil
    Thread.current[:start_time] = nil
  end
end

Inheritance between threads does not occur automatically. Child threads created within a thread do not inherit the parent thread's local variables. Each new thread starts with empty thread local storage.

# Thread local variables don't inherit
Thread.current[:parent_data] = "I am the parent"

child_thread = Thread.new do
  puts "Child sees: #{Thread.current[:parent_data].inspect}"
  # => nil - child threads start with empty storage
end

child_thread.join

Manual inheritance requires explicit copying of thread local data from parent to child threads. This copying creates separate storage spaces, not shared references.

# Manual thread local inheritance
def inherit_thread_locals(parent_thread)
  parent_thread.keys.each do |key|
    Thread.current[key] = parent_thread[key]
  end
end

Thread.current[:config] = { timeout: 30, retries: 3 }

child = Thread.new do
  inherit_thread_locals(Thread.main)
  puts "Child config: #{Thread.current[:config]}"
  # => {:timeout=>30, :retries=>3}
end

child.join

String and symbol keys create separate storage spaces in thread local variables. A string key and symbol key with the same name do not refer to the same storage location.

# String vs symbol key confusion
Thread.current[:user_id] = 123
Thread.current["user_id"] = 456

puts Thread.current[:user_id]    # => 123
puts Thread.current["user_id"]   # => 456
# These are completely separate variables!

Performance & Memory

Thread local variable access performs similarly to hash table operations since Ruby implements thread local storage as a hash table within each thread object. Reading and writing thread local variables has O(1) average-case performance, but the actual overhead depends on the number of keys stored and Ruby's hash table implementation.

# Performance comparison with different storage sizes
require 'benchmark'

def populate_thread_locals(count)
  count.times { |i| Thread.current["key_#{i}"] = "value_#{i}" }
end

def access_thread_locals(count)
  count.times { |i| Thread.current["key_#{i}"] }
end

Benchmark.bm(10) do |x|
  [100, 1000, 10000].each do |size|
    populate_thread_locals(size)
    
    x.report("#{size} reads") do
      10000.times { access_thread_locals(size) }
    end
  end
end

Memory consumption increases linearly with the number of thread local variables and the size of stored objects. Each thread maintains its own hash table, so memory usage multiplies across threads. Applications with many threads storing large objects can consume significant memory.

# Memory usage with multiple threads
def measure_memory_usage
  GC.start
  GC.disable
  
  before = GC.stat[:heap_allocated_pages]
  
  threads = 100.times.map do
    Thread.new do
      # Each thread stores 1000 strings
      1000.times { |i| Thread.current["data_#{i}"] = "x" * 1000 }
      sleep # Keep thread alive
    end
  end
  
  after = GC.stat[:heap_allocated_pages]
  threads.each(&:kill)
  
  puts "Additional pages allocated: #{after - before}"
ensure
  GC.enable
end

Frequent allocation and deallocation of thread local variables can impact garbage collection performance. Thread local variables prevent stored objects from being garbage collected until the thread terminates or the variable is explicitly set to nil.

# Impact on garbage collection
def create_garbage
  Thread.current[:large_data] = Array.new(100000) { |i| "data_#{i}" }
  
  # This doesn't help - reference still exists in thread local
  large_data = nil
  
  # Must explicitly clear thread local variable
  Thread.current[:large_data] = nil
end

# Proper cleanup for GC
def with_temp_data
  Thread.current[:temp] = expensive_computation
  yield Thread.current[:temp]
ensure
  Thread.current[:temp] = nil  # Allow GC
end

Thread creation and destruction costs increase when threads contain many thread local variables. Ruby must allocate and deallocate the hash table storage for each thread. Applications that frequently create short-lived threads with extensive thread local storage may experience performance degradation.

Reference

Thread Local Storage Methods

Method Parameters Returns Description
Thread.current[] key (String, Symbol, or Object) Object or nil Retrieves value stored with key
Thread.current[]= key (String, Symbol, or Object), value (Object) Object Stores value with key
Thread#[] key (String, Symbol, or Object) Object or nil Instance method for retrieving values
Thread#[]= key (String, Symbol, or Object), value (Object) Object Instance method for storing values
Thread#key? key (String, Symbol, or Object) Boolean Checks if key exists in thread local storage
Thread#keys None Array Returns array of all keys in thread local storage
Thread#fetch key (String, Symbol, or Object), default=nil Object Returns value or default if key missing

Thread Local Storage Characteristics

Characteristic Behavior
Key Types Strings, symbols, and any hashable object
Value Types Any Ruby object
Default Value nil for missing keys
Thread Safety Inherently thread-safe within owning thread
Inheritance Child threads start with empty storage
Cleanup Automatic when thread terminates
Memory Impact Linear with number of variables and object sizes

Common Patterns

Pattern Implementation Use Case
Context Storage Thread.current[:context] = data Request-specific data in web applications
Lazy Initialization `Thread.current[:obj]
Cleanup Handler ensure block with Thread.current[:key] = nil Preventing memory leaks
Configuration Thread.current.fetch(:config, DEFAULT_CONFIG) Thread-specific configuration
Scoped Access Wrapper class with getter/setter methods Controlled access to thread locals

Memory Management

Scenario Memory Behavior Mitigation
Long-running Threads Accumulates references indefinitely Explicit cleanup of unused variables
Thread Pools Data persists across task executions Clear variables between tasks
Large Objects Prevents garbage collection Set to nil when no longer needed
Many Threads Memory multiplies per thread Minimize thread local usage
Circular References May prevent thread termination Break references explicitly

Thread Pool Integration

# Safe thread pool usage pattern
def safe_pool_task
  setup_thread_locals
  yield
ensure
  cleanup_thread_locals
end

def setup_thread_locals
  Thread.current[:request_id] = SecureRandom.uuid
  Thread.current[:start_time] = Time.now
end

def cleanup_thread_locals
  Thread.current.keys.each { |key| Thread.current[key] = nil }
end