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.newcall immediately and resumes execution with the next statement. - Because the block supplied to
Thread.newis 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#valuemethod, 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
Mutexis locked by a thread using the calllock, and it is released using the callunlock. - Until the
lockis released, any further calls to lock aMutexthat 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
