Page Content

Tutorials

Concurrency In Ruby: Knowing Threads And Parallel Execution

Concurrency in Ruby

A Ruby concept called concurrency, which is implemented through threads, enables a program to run or appear to run multiple pieces of code concurrently. Threads provide a portable, effective, and lightweight alternative to the expensive, separate processes used by multitasking operating systems to achieve concurrency within a single Ruby process.

Basic Thread Semantics and Creation

Statements that run or appear to run in parallel with the main series of statements are called threads of execution.

Thread Creation and Execution

By attaching a code block to the Thread.new class method (or its synonyms, Thread.start and Thread.fork), new threads are generated.

  • As soon as CPU resources are available, a new thread starts up.
  • The original (main) thread returns from the Thread.new call immediately and resumes execution with the next statement.
  • Because the block supplied to Thread.new is a closure, it can access variables that were in scope at the time the thread was formed, enabling threads to share variables and communicate with one another.

Code Example 1: Basic Thread Creation (Doing Two Things at Once)

In this example, a variable is incremented by the main thread and concurrently decremented by a new thread.

x = 0
Thread.new do  # Thread 2 runs this code
  while x < 5
    x -= 1
    puts "DEC: I decremented x to #{x}\n"
  end
  puts "DEC: x is too high; I give up!\n"
end

while x < 5  # Thread 1 (main thread) runs this code
  x += 3
  puts "INC: I incremented x to #{x}\n"
end

Output

INC: I incremented x to 3
INC: I incremented x to 5

You can also read What Is Command Line Applications In Ruby With Examples

Waiting for Threads

The main thread is unique in that it terminates all other running threads early when it does, causing the Ruby interpreter to stop. This can be avoided by explicitly using Thread#join to wait for other threads to finish.

  • The caller thread is suspended until the specified thread terminates by using Thread#join.
  • The value of the final expression in the thread’s block is returned by the Thread#value method, which likewise blocks until the thread finishes.

Code Example 2: Using Thread#join

Thread#value implicitly calls the join in this concurrent file reading function to make sure all threads have finished reading before the main thread tries to return the results.

def readfiles(filenames)
  threads = filenames.map do |f|
    Thread.new { File.read(f) }   # Each thread reads a file
  end

  threads.map { |t| t.value }     # Return file contents
end

# ---- Test the method ----

# Create sample files
File.write("a.txt", "Hello A")
File.write("b.txt", "Hello B")

# Call the method and print the results
contents = readfiles(["a.txt", "b.txt"])
puts contents

Output

Hello A
Hello B

Thread-Local Variables

Threads automatically share variables that are defined in the surrounding scope. By passing it to the thread’s block parameters, you may make sure that a thread has a private variable that the main execution loop won’t change a common cause of errors.

Furthermore, thread-local variables can exhibit hash-like behaviour ([] and []=) thanks to the Thread class. Other threads can access these data by using the thread object, but they are not shared because each thread receives a copy of them.

Code Example 3: Using Thread-Local Variables

# Simulate download threads that update their progress
download_threads = 3.times.map do |i|
  Thread.new do
    total = 0
    5.times do
      sleep(0.1)               # simulate download time
      total += 10              # simulate bytes received
      Thread.current[:progress] = total
    end
  end
end

# Main thread monitors progress
loop do
  total_progress = download_threads.sum do |t|
    t[:progress].to_i
  end

  puts "Total progress across threads: #{total_progress} bytes"

  break if download_threads.all?(&:stop?)
end

Output

Total progress across threads: 0 bytes

You can also read Building Your First Website Sinatra In Ruby With Examples

Thread Synchronization: Avoiding Data Corruption

To guarantee that the data is never viewed in an inconsistent state, thread exclusion, also known as synchronization, is required when multiple threads read and write the same shared data. Inconsistent data reading or missed updates might result from race situations caused by a failure to synchronize.

Mutex (Mutual Exclusion)

For synchronization, the Mutex object is the main tool.

  • The Mutex is locked by a thread using the call lock, and it is released using the call unlock.
  • Until the lock is released, any further calls to lock a Mutex that is already locked will be blocked.

The synchronize method and a related block are the most common and secure ways to use a mutex. Synchronize locks the Mutex, runs the block’s code, and makes sure the Mutex is unlocked even in the event of an exception (by using an ensure clause).

Code Example 4: Synchronizing Access to a Counter

Throughout the read-increment-write cycle, a shared counter needs to be secured.

require 'thread'

counter = 0
counter_mutex = Mutex.new   # Controls access to counter

# Start three parallel threads
threads = 3.times.map do
  Thread.new do
    counter_mutex.synchronize do
      counter += 1           # Safe increment
    end
  end
end

threads.each(&:join)

# Print final value
puts counter

Output

3

Race situations would make the final counter value unreliable if the Mutex#synchronize block wasn’t present.

You can also read Web Application Frameworks In Ruby: Power Of Ruby On Rails

Thread-Safe Queues

Queue and SizedQueue classes offered by the standard thread library are especially made for producer/consumer concurrent programming. By nature, they are thread-safe.

#push / #enq: Adds an object to the queue. (Note: #push never blocks on a standard Queue).

#pop / #deq / #shift: Removes and returns an object from the queue. The key feature is that #pop will block if the queue is empty, waiting until a producer thread adds a value. This blocking feature can be used for synchronization.

Code Example 5: Producer/Consumer Model using Queue

This illustrates how several worker threads (producers) create random numbers, add them to a queue (sink), and then build an array from the queue (consumer).

require 'thread'
sink = Queue.new

# Producers: 16 threads generate numbers and push to sink
threads = (1..16).map do
  Thread.new do
    sink << rand(1..100)
  end
end

threads.each(&:join)

# Consumer: Convert Queue into an Array
data = []
data << sink.pop until sink.empty?

# Print the data
p data

Output

[38, 67, 69, 77, 24, 6, 9, 67, 51, 99, 20, 74, 57, 86, 7, 90]

You can also read What Is Automated Testing for Web Applications In Ruby

Thread Implementation and Concurrency Context

Ruby Implementation and Performance

The implementation of the interpreter has a significant impact on the concurrent behaviour of Ruby threads.

MRI 1.8 (Standard C Implementation): Employed “green threads” pseudo threads that operate inside a single native thread. Despite being lightweight, threads never operated in true parallel, even on CPUs with several cores. Gets and puts are examples of long-running C language calls that would block all other threads.

MRI 1.9: Uses Global Interpreter Lock (GIL)-enabled native threads. In order to limit execution to a single native thread running Ruby code at a time, it allots a native thread for every Ruby thread. As a result, real parallelism is still mostly faked.

JRuby / IronRuby: On multicore CPUs, these implementations can accomplish real parallel processing by utilizing true concurrent native threads.

Application Scenarios for Concurrency

I/O-bound programs (such as web browsers waiting for disc access or network traffic) typically benefit most from threads since they allow for the successful simulation of parallelism while one thread is stalled while it awaits an external resource.

Concurrent Operations: High-latency tasks (such as file reads or network queries) are frequently carried out concurrently using threads to minimise total elapsed time. Ten nameserver lookups, for instance, may take 50 seconds when performed in sequence, but just 5 seconds when performed in parallel.

Multithreaded Servers: Writing servers with several clients operating in separate threads at the same time is a canonical use case.

Code Example 6: Multithreaded Server

This simple server uses a separate thread to manage every new client connection:

require 'socket'

def handle_client(c) # Handles communication with a single client 
  while true
    input = c.gets.chop
    break if !input || input=="quit"
    c.puts(input.reverse)
    c.flush
  end
  c.close
end

server = TCPServer.open(2000)
while true
  client = server.accept        # Wait for a client to connect 
  Thread.start(client) do |c|   # Start a new thread for the client
    handle_client(c)
  end
end

You can also read What Is Cookies And Sessions In Ruby, key Differences Of It

Agarapu Geetha
Agarapu Geetha
My name is Agarapu Geetha, a B.Com graduate with a strong passion for technology and innovation. I work as a content writer at Govindhtech, where I dedicate myself to exploring and publishing the latest updates in the world of tech.
Index