DEV Community

francesco agati
francesco agati

Posted on

Concurrency and Parallelism in Ruby

Concurrency and Parallelism in Ruby

In programming, concurrency and parallelism are essential techniques for improving the performance and efficiency of code. Ruby, a popular programming language, offers various tools to handle these concepts. Let's explore these techniques using a simple example.

Synchronous Code

Synchronous code executes tasks one after the other. Here's an example:

puts "Synchronous Code"
(1..5).each do |i|
  puts i
  sleep 1
end
Enter fullscreen mode Exit fullscreen mode

In this code, numbers from 1 to 5 are printed with a 1-second delay between each number. The tasks run sequentially, meaning each number is printed only after the previous task (including the sleep) is completed.

Threads

Threads allow multiple sequences of instructions to run concurrently within the same program. Here's how we can use threads in Ruby:

puts "\nThreads"
threads = []
(1..5).each do |i|
  threads << Thread.new do
    puts i
    sleep 1
  end
end
threads.each(&:join)
Enter fullscreen mode Exit fullscreen mode

In this example, each number from 1 to 5 is printed by a separate thread. All threads run concurrently, and the join method ensures that the main program waits for all threads to finish before proceeding. This makes the tasks run in parallel, potentially reducing the total execution time.

Fork

The fork method creates a new process, which is a separate instance of the Ruby interpreter:

puts "\nFork"
(1..5).each do |i|
  pid = fork do
    puts i
    sleep 1
  end
  Process.wait(pid)
end
Enter fullscreen mode Exit fullscreen mode

In this code, fork creates a new process for each number. The parent process waits for each child process to complete using Process.wait. Each process runs independently, providing true parallelism on multi-core systems.

Fibers

Fibers are lightweight concurrency primitives that enable cooperative multitasking:

puts "\nFibers"
fibers = []
(1..5).each do |i|
  fibers << Fiber.new do
    puts i
    sleep 1
    Fiber.yield
  end
end
fibers.each(&:resume)
Enter fullscreen mode Exit fullscreen mode

Each fiber runs a block of code and can be paused and resumed. This example prints numbers 1 to 5, pausing after each number. Although fibers provide concurrency, they do not run in parallel; the main program controls when each fiber resumes.

Ractor (Ruby 3)

Ractors, introduced in Ruby 3, enable true parallel execution by running code in isolated compartments:

puts "\nRactor"
ractors = (1..5).map do |i|
  Ractor.new(i) do |i|
    sleep 1
    puts i
  end
end
ractors.each(&:take)
Enter fullscreen mode Exit fullscreen mode

In this example, each number from 1 to 5 is printed by a separate ractor. Ractors can run in parallel, making full use of multi-core processors. The take method waits for each ractor to finish and return its result.

When to Use Concurrency and Parallelism Techniques

Understanding when to use each concurrency and parallelism technique is crucial for optimizing performance in Ruby applications. Here's a guide:

Threads and Fibers

  • Threads: Use threads when you need to handle I/O-bound tasks, such as reading and writing files or making network requests. Threads can run concurrently but are limited by Ruby's Global Interpreter Lock (GIL), which means only one thread executes Ruby code at a time. Threads are heavier than fibers, requiring more resources, but they are suitable for tasks that involve waiting for external data.

  • Fibers: Fibers are even lighter than threads and are used for cooperative multitasking. A single thread can manage multiple fibers, making fibers ideal for managing multiple I/O-bound tasks without creating additional threads. Fibers need explicit control for yielding and resuming, which provides fine-grained control over task execution.

Fork and Ractor

  • Fork: Use fork for CPU-bound tasks that require significant computation and can benefit from true parallelism. Forking creates a new process, allowing it to run on a separate CPU core without being limited by the GIL. This is useful for heavy computations but incurs more overhead due to process creation and inter-process communication.

  • Ractor: Introduced in Ruby 3, ractors provide a way to achieve parallelism while ensuring thread safety. Ractors are ideal for heavy computations that can be distributed across multiple CPU cores. Unlike threads, ractors do not share memory and communicate via message passing, avoiding issues with the GIL and improving performance on multi-core systems.

Ruby provides multiple ways to handle concurrency and parallelism, each suited for different scenarios. Synchronous code is simple but sequential. Threads and fibers allow for concurrent execution within a single process, with threads offering parallelism on multi-core systems. Forking creates new processes for true parallelism, while ractors offer a modern and thread-safe way to achieve parallel execution in Ruby 3. Understanding these techniques helps developers write efficient and performant Ruby programs.

Top comments (3)

Collapse
 
katafrakt profile image
Paweł Świątkowski

I have been using Ruby for like 15 years and I still have to find a convincing use case for fibers.

Collapse
 
francescoagati profile image
francesco agati

see falcon

Collapse
 
thesmartnik profile image
TheSmartnik

I've seen it being used in a "download pause" of a http client