CrackedRuby CrackedRuby

Overview

Thread-Local Storage (TLS) provides each thread in a program with its own isolated copy of specific variables. When a thread accesses a thread-local variable, it retrieves or modifies only its own copy, preventing data races and eliminating the need for synchronization mechanisms like locks or mutexes for those specific variables.

The concept emerged from the need to maintain per-thread state in concurrent programs without the overhead and complexity of explicit synchronization. Operating systems implement TLS at a low level, typically using special registers or dedicated memory regions indexed by thread identifiers. High-level languages expose this functionality through various APIs, allowing developers to declare variables that exist independently for each thread.

Thread-local storage solves specific problems in concurrent programming:

State Isolation: Each thread maintains its own version of a variable, preventing accidental sharing and data corruption. When Thread A modifies a thread-local variable, Thread B's copy remains unchanged.

Context Propagation: Framework code can store request-specific data (user sessions, transaction IDs, logging contexts) in thread-local storage, making this data accessible throughout the call stack without passing it as parameters.

Performance Optimization: Thread-local caches and buffers eliminate contention that would occur if threads shared these resources, improving throughput in multi-threaded applications.

# Basic thread-local storage demonstration
Thread.current[:request_id] = "req-123"

def process_data
  # Access thread-local data without passing parameters
  puts "Processing in request: #{Thread.current[:request_id]}"
end

process_data  # => "Processing in request: req-123"

The implementation varies across programming environments. Some languages provide dedicated thread-local variable declarations, while others offer APIs for storing and retrieving thread-scoped data. Ruby uses the Thread class with hash-like access for thread-local storage, along with fiber-local variables for more granular control in fiber-based concurrency.

Key Principles

Thread-local storage operates on the principle of thread-specific memory allocation. When a program declares a variable as thread-local, the runtime system allocates separate memory locations for that variable in each thread. The system maintains a mapping between thread identifiers and memory locations, ensuring each thread accesses only its own copy.

Isolation Boundaries: Thread-local variables exist within the scope of a single thread's execution. When a thread terminates, its thread-local storage becomes eligible for garbage collection (in managed languages) or must be explicitly freed (in manual memory management systems). This lifecycle ties thread-local data directly to thread lifetime.

Access Patterns: Accessing thread-local storage involves a lookup operation using the current thread's identifier. Modern implementations optimize this with techniques like:

  • Special processor registers dedicated to thread-local storage pointers
  • Inline caching of frequently accessed thread-local variables
  • Compile-time resolution of thread-local addresses when possible

Memory Layout: Thread-local storage typically resides in one of three locations:

  1. Static TLS: Allocated at program load time, with fixed offsets for each thread-local variable. Fast access but limited to variables known at compile time.

  2. Dynamic TLS: Allocated at runtime when threads create thread-local variables. Slower access but supports runtime-determined thread-local storage.

  3. Thread Control Block: A per-thread data structure maintained by the runtime that holds references to thread-local storage segments.

Initialization Semantics: Thread-local variables follow specific initialization rules. In most implementations, when a new thread starts, its thread-local variables receive either:

  • Default values (zero for primitives, null for references)
  • Values from an initialization function called once per thread
  • Copies of values from a parent thread (in some fork-based concurrency models)
# Thread-local initialization in Ruby
Thread.current[:counter] ||= 0

5.times do
  Thread.current[:counter] += 1
end

puts Thread.current[:counter]  # => 5

# New thread starts with nil
Thread.new do
  puts Thread.current[:counter].inspect  # => nil
end.join

Visibility Constraints: Thread-local storage creates hard isolation boundaries. A variable stored in one thread's local storage cannot be accessed by another thread through the same variable name. This prevents common concurrency bugs but requires explicit mechanisms for inter-thread communication when needed.

Fiber Interactions: In languages supporting lightweight concurrency primitives like fibers or green threads, thread-local storage behavior requires careful specification. Some systems provide fiber-local storage as a separate mechanism, while others make thread-local storage accessible across all fibers within a thread.

# Ruby distinguishes thread-local and fiber-local storage
Thread.current[:shared] = "thread-level"
Thread.current.thread_variable_set(:truly_local, "thread-only")

Fiber.new do
  puts Thread.current[:shared]  # => "thread-level" (accessible in fiber)
  puts Thread.current.thread_variable_get(:truly_local)  # => "thread-only"
  
  Thread.current[:shared] = "modified"
end.resume

puts Thread.current[:shared]  # => "modified" (change visible across fibers)

Ruby Implementation

Ruby provides multiple mechanisms for thread-local storage, each with different scoping and inheritance characteristics. The primary interface uses Thread.current with hash-like access, while thread variables offer stricter isolation.

Thread.current Hash Access: The most common approach treats Thread.current as a hash, storing key-value pairs specific to the current thread.

# Storing thread-local data
Thread.current[:user_id] = 42
Thread.current[:session] = { token: "abc123" }

# Retrieving thread-local data
user = Thread.current[:user_id]
session = Thread.current[:session]

# Multiple threads maintain separate copies
threads = 5.times.map do |i|
  Thread.new do
    Thread.current[:thread_num] = i
    sleep 0.01
    puts "Thread #{Thread.current[:thread_num]}"
  end
end

threads.each(&:join)
# Output shows each thread maintains its own value

Thread Variables: Ruby 2.0 introduced thread variables via thread_variable_set and thread_variable_get, providing true thread-local storage that fibers cannot access or modify.

Thread.current.thread_variable_set(:count, 0)

# Thread variables don't leak into fibers
Fiber.new do
  Thread.current.thread_variable_set(:count, 10)
  puts Thread.current.thread_variable_get(:count)  # => 10
end.resume

puts Thread.current.thread_variable_get(:count)  # => 0 (unchanged)

# Regular thread-local storage (hash access) shares with fibers
Thread.current[:shared_count] = 0

Fiber.new do
  Thread.current[:shared_count] = 10
end.resume

puts Thread.current[:shared_count]  # => 10 (modified by fiber)

RequestStore Pattern: The RequestStore gem provides a standard pattern for request-scoped storage in web applications, handling thread pool reuse correctly.

require 'request_store'

# Safe for thread pool environments
RequestStore.store[:current_user] = User.find(params[:id])

# Automatically cleans up between requests
# Handles Rack middleware integration

def current_user
  RequestStore.store[:current_user]
end

Thread-Local Singleton Pattern: Creating singleton instances per thread requires careful initialization.

class ThreadLocalCache
  def self.instance
    Thread.current[:cache_instance] ||= new
  end
  
  def initialize
    @data = {}
  end
  
  def get(key)
    @data[key]
  end
  
  def set(key, value)
    @data[key] = value
  end
end

# Each thread gets its own cache instance
thread1 = Thread.new do
  cache = ThreadLocalCache.instance
  cache.set("key", "value1")
  puts cache.get("key")  # => "value1"
end

thread2 = Thread.new do
  cache = ThreadLocalCache.instance
  puts cache.get("key").inspect  # => nil (different instance)
  cache.set("key", "value2")
end

thread1.join
thread2.join

Thread-Local Database Connections: Managing per-thread database connections prevents connection sharing issues.

class ConnectionPool
  def self.connection
    Thread.current[:db_connection] ||= establish_connection
  end
  
  def self.establish_connection
    # Create new connection for this thread
    Database.connect(
      host: 'localhost',
      pool: false  # Disable connection pooling
    )
  end
  
  def self.close_connection
    conn = Thread.current[:db_connection]
    conn&.close
    Thread.current[:db_connection] = nil
  end
end

# Usage in threaded environment
threads = 10.times.map do
  Thread.new do
    conn = ConnectionPool.connection
    result = conn.execute("SELECT * FROM users")
    # Each thread uses its own connection
  ensure
    ConnectionPool.close_connection
  end
end

threads.each(&:join)

ActiveSupport::CurrentAttributes: Rails 5.2+ provides a framework for thread-local attributes with automatic cleanup.

class Current < ActiveSupport::CurrentAttributes
  attribute :user, :request_id, :user_agent
  
  def user=(user)
    super
    Time.zone = user.timezone
  end
end

# Set thread-local attributes
Current.user = User.find(session[:user_id])
Current.request_id = request.uuid

# Access anywhere in the call stack
def process_order(order)
  order.processed_by = Current.user
  logger.info("Processing order", request_id: Current.request_id)
end

# Automatically resets between requests in Rails

Practical Examples

Web Request Context: Web frameworks commonly use thread-local storage to maintain request-scoped data accessible throughout the request lifecycle.

class RequestContext
  def self.set(key, value)
    Thread.current[:request_context] ||= {}
    Thread.current[:request_context][key] = value
  end
  
  def self.get(key)
    Thread.current[:request_context]&.fetch(key, nil)
  end
  
  def self.clear
    Thread.current[:request_context] = {}
  end
end

# Middleware sets context
class ContextMiddleware
  def call(env)
    RequestContext.clear
    RequestContext.set(:request_id, SecureRandom.uuid)
    RequestContext.set(:start_time, Time.now)
    
    status, headers, body = @app.call(env)
    
    duration = Time.now - RequestContext.get(:start_time)
    logger.info(
      "Request completed",
      request_id: RequestContext.get(:request_id),
      duration: duration
    )
    
    [status, headers, body]
  ensure
    RequestContext.clear
  end
end

# Controllers access context without passing parameters
class OrdersController
  def create
    order = Order.create(order_params)
    order.request_id = RequestContext.get(:request_id)
    order.save
  end
end

Thread-Local Logger Context: Adding request-specific information to all log messages without modifying every logging call.

class ThreadLocalLogger
  def self.with_context(context = {})
    previous = Thread.current[:log_context] || {}
    Thread.current[:log_context] = previous.merge(context)
    yield
  ensure
    Thread.current[:log_context] = previous
  end
  
  def self.log(level, message)
    context = Thread.current[:log_context] || {}
    full_message = context.merge(message: message)
    logger.send(level, full_message.to_json)
  end
end

# Usage
ThreadLocalLogger.with_context(user_id: current_user.id, request_id: request.uuid) do
  ThreadLocalLogger.log(:info, "Processing payment")
  process_payment
  ThreadLocalLogger.log(:info, "Payment completed")
end

# All logs include user_id and request_id automatically

Per-Thread Performance Metrics: Collecting metrics without synchronization overhead.

class ThreadMetrics
  def self.increment(metric_name, value = 1)
    metrics = Thread.current[:metrics] ||= Hash.new(0)
    metrics[metric_name] += value
  end
  
  def self.timing(metric_name)
    start = Time.now
    yield
  ensure
    duration = Time.now - start
    increment("#{metric_name}_time", duration)
    increment("#{metric_name}_count")
  end
  
  def self.report
    Thread.current[:metrics] || {}
  end
  
  def self.reset
    Thread.current[:metrics] = Hash.new(0)
  end
end

# Usage in worker threads
worker = Thread.new do
  ThreadMetrics.reset
  
  100.times do
    ThreadMetrics.timing(:process_item) do
      process_item
    end
  end
  
  metrics = ThreadMetrics.report
  puts "Processed #{metrics[:process_item_count]} items"
  puts "Average time: #{metrics[:process_item_time] / metrics[:process_item_count]}"
end

worker.join

Transaction Context Management: Maintaining database transaction state per thread in a connection pool environment.

class TransactionManager
  def self.begin_transaction
    raise "Transaction already active" if active?
    
    Thread.current[:transaction] = {
      connection: ConnectionPool.acquire,
      savepoints: [],
      started_at: Time.now
    }
    
    connection.begin_transaction
  end
  
  def self.commit
    raise "No active transaction" unless active?
    
    transaction = Thread.current[:transaction]
    transaction[:connection].commit
    
    ConnectionPool.release(transaction[:connection])
    Thread.current[:transaction] = nil
  end
  
  def self.rollback
    return unless active?
    
    transaction = Thread.current[:transaction]
    transaction[:connection].rollback
    
    ConnectionPool.release(transaction[:connection])
    Thread.current[:transaction] = nil
  end
  
  def self.active?
    Thread.current[:transaction].present?
  end
  
  def self.connection
    transaction = Thread.current[:transaction]
    raise "No active transaction" unless transaction
    transaction[:connection]
  end
  
  def self.with_transaction
    begin_transaction
    yield
    commit
  rescue StandardError => e
    rollback
    raise
  end
end

# Usage
TransactionManager.with_transaction do
  User.create(name: "Alice")
  Order.create(user_id: user.id)
  # Both operations use the same connection from thread-local storage
end

Design Considerations

When to Use Thread-Local Storage: Thread-local storage fits specific scenarios where thread-specific state must persist across function calls without explicit parameter passing. Request-scoped data in web applications represents the primary use case—storing user authentication, request identifiers, or transaction contexts that the entire request handler stack needs to access.

Temporary caching within thread execution contexts also benefits from thread-local storage. Threads performing repeated operations can maintain caches that live for the thread's duration, avoiding contention that shared caches would introduce. Format parsers, date formatters, and other stateful objects that are expensive to create but not thread-safe become viable when each thread maintains its own instance.

When to Avoid Thread-Local Storage: Thread-local storage introduces hidden state that makes code harder to test and reason about. Functions that depend on thread-local variables become harder to understand because their behavior depends on external context not visible in the function signature. This violates principles of explicit dependencies and function purity.

Thread pool environments create additional complications. Application servers and background job processors reuse threads across multiple requests or jobs. Thread-local variables set during one request persist when the thread handles the next request, causing data leakage between unrelated operations. Frameworks that use thread pools require careful cleanup of thread-local storage between operations.

Memory leaks emerge easily with thread-local storage. Storing references to large objects or maintaining caches without size limits causes memory consumption to grow unbounded in long-lived threads. Thread-local storage survives longer than individual request processing might suggest, particularly in thread pool scenarios.

Alternatives to Thread-Local Storage: Explicit parameter passing remains the clearest approach for providing context to functions. While more verbose, parameters make dependencies visible and testable. Dependency injection frameworks formalize this pattern, managing object lifecycles and scopes without hidden global state.

Context objects passed through call chains provide a middle ground. Rack middleware passes an env hash through the request cycle, containing all request-specific data. This approach maintains explicitness while avoiding the need to modify every function signature as new context requirements emerge.

Immutable context objects combined with functional programming patterns eliminate many concurrency concerns entirely. When functions receive all required context as immutable data and produce new immutable results, thread-local storage becomes unnecessary. This approach proves particularly effective in languages with strong support for immutable data structures.

Trade-off Analysis: Thread-local storage optimizes convenience over explicitness. Code becomes more concise but less transparent. Testing becomes more complex because test setup must initialize thread-local state. This trade-off proves worthwhile when:

  • Many functions deep in the call stack need access to the same context
  • The context represents framework-level concerns rather than application logic
  • The application architecture already commits to thread-per-request concurrency
  • Performance requirements preclude parameter passing overhead for high-frequency operations

The trade-off proves unfavorable when:

  • The application uses thread pools or fiber-based concurrency
  • Function behavior should remain testable in isolation
  • Code maintainability and clarity take priority over convenience
  • Debugging and tracing require understanding function dependencies

Integration with Concurrency Models: Different concurrency models interact with thread-local storage in distinct ways. Thread-per-request models work naturally with thread-local storage since each request executes in a dedicated thread. Event-driven architectures using event loops and callbacks break thread-local storage assumptions because a single thread handles many concurrent operations.

Fiber-based concurrency creates ambiguity about whether "thread-local" means thread-scoped or fiber-scoped. Ruby addresses this by providing both thread-local storage accessible across fibers and true thread variables isolated from fibers. Applications using fibers must choose the appropriate scope for each piece of state.

Actor-based concurrency systems render thread-local storage largely irrelevant since actors encapsulate state and communicate through message passing. The actor model's design explicitly avoids shared state, making thread-local storage an anti-pattern in that context.

Common Patterns

Registry Pattern: Maintaining a thread-local registry of services or configuration objects that functions throughout the call stack can access.

class ServiceRegistry
  def self.register(name, service)
    registry[name] = service
  end
  
  def self.lookup(name)
    registry[name] || raise("Service not registered: #{name}")
  end
  
  def self.clear
    Thread.current[:service_registry] = {}
  end
  
  private
  
  def self.registry
    Thread.current[:service_registry] ||= {}
  end
end

# Framework initialization
ServiceRegistry.clear
ServiceRegistry.register(:logger, Logger.new(STDOUT))
ServiceRegistry.register(:cache, RedisCache.new)

# Application code accesses services
class OrderProcessor
  def process(order)
    logger = ServiceRegistry.lookup(:logger)
    cache = ServiceRegistry.lookup(:cache)
    
    logger.info("Processing order #{order.id}")
    cached_data = cache.get("order:#{order.id}")
  end
end

Context Stack Pattern: Maintaining a stack of contexts in thread-local storage to support nested operations that each establish their own context.

class ContextStack
  def self.push(context)
    stack.push(context)
  end
  
  def self.pop
    stack.pop
  end
  
  def self.current
    stack.last
  end
  
  def self.with_context(context)
    push(context)
    yield
  ensure
    pop
  end
  
  private
  
  def self.stack
    Thread.current[:context_stack] ||= []
  end
end

# Nested context usage
ContextStack.with_context(user: admin_user) do
  # Admin context
  
  ContextStack.with_context(impersonating: regular_user) do
    # Nested context for impersonation
    actual_user = ContextStack.current[:user]  # admin_user
    impersonated = ContextStack.current[:impersonating]  # regular_user
  end
  
  # Back to admin context
end

Lazy Initialization Pattern: Creating thread-local objects on first access to avoid initialization overhead for threads that never use the object.

class ThreadLocalFormatter
  def self.instance
    Thread.current[:formatter] ||= begin
      formatter = DateTimeFormatter.new
      formatter.configure(
        timezone: ENV['TIMEZONE'],
        format: :iso8601
      )
      formatter
    end
  end
end

# First access initializes, subsequent accesses reuse
100.times do
  formatted = ThreadLocalFormatter.instance.format(Time.now)
end

Automatic Cleanup Pattern: Registering cleanup handlers to ensure thread-local storage gets cleared when threads terminate or between operations.

class ThreadLocalManager
  def self.register_cleanup(&block)
    cleanups << block
  end
  
  def self.cleanup
    cleanups.each(&:call)
    Thread.current[:cleanup_handlers] = []
  end
  
  private
  
  def self.cleanups
    Thread.current[:cleanup_handlers] ||= []
  end
end

# Registering resources that need cleanup
class DatabaseConnection
  def self.for_thread
    Thread.current[:db_conn] ||= begin
      conn = establish_connection
      
      ThreadLocalManager.register_cleanup do
        conn.close
        Thread.current[:db_conn] = nil
      end
      
      conn
    end
  end
end

# Framework ensures cleanup
Thread.new do
  conn = DatabaseConnection.for_thread
  perform_work
ensure
  ThreadLocalManager.cleanup
end

Default Value Pattern: Providing sensible defaults for thread-local storage when values haven't been explicitly set.

class Configuration
  DEFAULTS = {
    timeout: 30,
    retry_count: 3,
    log_level: :info
  }
  
  def self.get(key)
    config = Thread.current[:config] ||= {}
    config.fetch(key, DEFAULTS[key])
  end
  
  def self.set(key, value)
    config = Thread.current[:config] ||= {}
    config[key] = value
  end
  
  def self.with_overrides(overrides)
    previous = Thread.current[:config]
    Thread.current[:config] = (previous || {}).merge(overrides)
    yield
  ensure
    Thread.current[:config] = previous
  end
end

# Use defaults or override
Configuration.get(:timeout)  # => 30

Configuration.with_overrides(timeout: 60) do
  Configuration.get(:timeout)  # => 60
end

Configuration.get(:timeout)  # => 30

Common Pitfalls

Thread Pool Contamination: Thread pools reuse threads across multiple operations, causing thread-local variables set during one operation to persist into the next. This creates subtle bugs where data from one request appears in another.

# Problematic code
class UserContext
  def self.current_user=(user)
    Thread.current[:current_user] = user
  end
  
  def self.current_user
    Thread.current[:current_user]
  end
end

# In a threaded web server with connection pooling
def handle_request(request)
  user = authenticate(request)
  UserContext.current_user = user
  
  process_request(request)
  # Missing cleanup—user persists in thread-local storage
end

# Next request in same thread sees previous user
def handle_next_request(unauthenticated_request)
  # Bug: UserContext.current_user returns previous request's user
  if UserContext.current_user
    # Incorrectly authorized because thread-local wasn't cleared
    access_protected_resource
  end
end

# Solution: Always clean up
def handle_request(request)
  user = authenticate(request)
  UserContext.current_user = user
  
  process_request(request)
ensure
  UserContext.current_user = nil  # Critical cleanup
end

Memory Leaks in Long-Lived Threads: Thread-local storage persists for the thread's entire lifetime. In application servers with long-lived worker threads, thread-local variables accumulate memory without bounds if not managed carefully.

# Memory leak example
class RequestCache
  def self.cache
    Thread.current[:request_cache] ||= {}
  end
  
  def self.memoize(key)
    cache[key] ||= yield
  end
end

# Each request adds to the cache, never clearing
1000.times do |i|
  RequestCache.memoize("user_#{i}") do
    User.find(i)  # Cached forever in thread-local storage
  end
end

# Solution: Bounded cache with cleanup
class BoundedRequestCache
  MAX_SIZE = 100
  
  def self.memoize(key)
    cache = Thread.current[:request_cache] ||= {}
    
    if cache.size >= MAX_SIZE
      cache.clear  # Prevent unbounded growth
    end
    
    cache[key] ||= yield
  end
end

Testing Complexity: Thread-local storage creates hidden dependencies that complicate testing. Tests must set up thread-local state before each test and clean up afterward, or tests interfere with each other.

# Difficult to test
class OrderProcessor
  def process(order)
    user = UserContext.current_user  # Hidden dependency
    order.processed_by = user.id
    order.save
  end
end

# Test requires thread-local setup
RSpec.describe OrderProcessor do
  before do
    UserContext.current_user = build(:user)
  end
  
  after do
    UserContext.current_user = nil
  end
  
  it "processes order" do
    processor = OrderProcessor.new
    processor.process(order)
    # Test depends on thread-local state being set correctly
  end
end

# Better design with explicit dependencies
class OrderProcessor
  def process(order, user:)
    order.processed_by = user.id
    order.save
  end
end

# Test is simpler and clearer
RSpec.describe OrderProcessor do
  it "processes order" do
    user = build(:user)
    processor = OrderProcessor.new
    processor.process(order, user: user)
    # No thread-local setup required
  end
end

Fiber Confusion: Ruby's thread-local storage using Thread.current hash access is actually fiber-local, causing unexpected behavior when mixing threads and fibers.

# Unexpected behavior with fibers
Thread.current[:value] = "main fiber"

Fiber.new do
  Thread.current[:value] = "other fiber"
end.resume

puts Thread.current[:value]  # => "other fiber" (modified by fiber!)

# Use thread variables for true thread-local storage
Thread.current.thread_variable_set(:value, "main fiber")

Fiber.new do
  Thread.current.thread_variable_set(:value, "other fiber")
end.resume

puts Thread.current.thread_variable_get(:value)  # => "main fiber" (unchanged)

Race Conditions in Initialization: Lazy initialization of thread-local storage can create race conditions if the initialization code isn't thread-safe or if multiple threads initialize the same conceptual resource.

# Problematic lazy initialization
class ConnectionManager
  def self.connection
    Thread.current[:connection] ||= Database.connect
  end
end

# If Database.connect isn't thread-safe, multiple threads
# might corrupt shared state during initialization

# Safe approach: Ensure initialization is thread-safe
class ConnectionManager
  def self.connection
    Thread.current[:connection] ||= begin
      # Use thread-safe initialization
      connection = Database.connect
      connection.set_isolation_level(:read_committed)
      connection
    end
  end
end

Forgetting Cleanup in Exception Paths: Exception handling can skip thread-local cleanup, leaving stale state in thread pools.

# Cleanup skipped on exception
def process_request(request)
  RequestContext.set(:user, authenticate(request))
  handle_request(request)
  RequestContext.clear  # Never reached if handle_request raises
end

# Proper cleanup with ensure
def process_request(request)
  RequestContext.set(:user, authenticate(request))
  handle_request(request)
ensure
  RequestContext.clear  # Always executes
end

Debugging Difficulties: Thread-local storage makes debugging harder because inspecting variables requires examining thread-specific state that may not be visible in standard debugging tools.

# Hard to debug
def process_data
  cache = ThreadLocalCache.instance
  # Debugger may not show thread-local cache contents easily
  result = cache.get(key)
end

# Add explicit debugging support
class ThreadLocalCache
  def self.debug_info
    {
      thread_id: Thread.current.object_id,
      cache_size: instance.size,
      cache_keys: instance.keys
    }
  end
end

# Debug by inspecting thread-local state explicitly
puts ThreadLocalCache.debug_info.inspect

Reference

Ruby Thread-Local Storage API

Method Purpose Scope
Thread.current[:key] Store and retrieve fiber-local data Current thread and its fibers
Thread.current[:key] = value Set fiber-local variable Current thread and its fibers
Thread.current.thread_variable_set Set true thread-local variable Current thread only, not fibers
Thread.current.thread_variable_get Get true thread-local variable Current thread only, not fibers
Thread.current.thread_variables List all thread variable keys Current thread only
Thread.current.thread_variable? Check if thread variable exists Current thread only

Storage Scope Comparison

Storage Type Accessible from Fibers Persists Across Fiber Switches Use Case
Thread.current[:key] Yes Yes Request-scoped data in fiber-based apps
Thread.current.thread_variable_set No N/A True thread-local isolation
Instance variables on Thread.current Yes Yes Complex thread-local objects
Fiber local storage Fiber only No Fiber-specific state

Common Pitfalls and Solutions

Pitfall Symptom Solution
Thread pool contamination Data leaks between requests Clear thread-locals in ensure blocks
Memory leaks Growing memory usage Implement bounded caches, cleanup on thread exit
Fiber confusion Unexpected value changes Use thread_variable_set for true thread-locals
Testing interference Flaky tests Reset thread-locals in test setup/teardown
Hidden dependencies Hard to test and debug Prefer explicit parameter passing
Race conditions Intermittent initialization bugs Ensure initialization code is thread-safe

Design Decision Matrix

Requirement Thread-Local Storage Parameter Passing Context Object
Explicitness Low High Medium
Testability Difficult Easy Easy
Framework-level context Excellent Poor Good
Thread pool safety Requires cleanup Safe Safe
Fiber compatibility Depends on API Safe Safe
Memory efficiency Risk of leaks Efficient Efficient
Debugging clarity Poor Excellent Good

Cleanup Patterns

Pattern Code Template When to Use
Ensure block begin; set_context; work; ensure; clear; end Single operation cleanup
Middleware before: set; after: clear Request-scoped cleanup
Resource wrapper with_context { work } Scoped resource management
Thread finalizer Thread.new { work; ensure cleanup } Thread termination cleanup
Periodic cleanup Every N operations, clear cache Long-running threads

Framework Integration

Framework Thread-Local Support Cleanup Mechanism
Rails ActiveSupport::CurrentAttributes Automatic per-request
Rack Middleware-based Manual middleware
Sidekiq Per-job isolation Automatic job cleanup
Puma Thread pool with cleanup Manual ensure blocks
RequestStore gem Request-scoped storage Automatic middleware