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