Skip to Content
Course content

98: Threads in Ruby and the GIL

Click on the "Edit" button in the top corner of the screen to edit your slide content.

How do I actually launch a thread and get a result back from it?

In Ruby, you start a thread using Thread.new. You pass it a block of code, and Ruby hands that block off to the operating system to be executed independently of your main program flow. The tricky part for beginners is that the main Ruby process doesn't wait for these threads to finish; if your main script reaches the end of the file, it'll just kill any running threads and exit.

To prevent that, you use the .join method. Think of it as telling your main program, "Hold on, don't move forward until this specific thread is done."

# Let's simulate fetching data from three different APIs
urls = ["api.weather.com", "api.stocks.com", "api.news.com"]
threads = []

urls.each do |url|
  threads << Thread.new do
    puts "Fetching from #{url}..."
    sleep(rand(1..3)) # Simulate network latency
    "Data from #{url}" # The last line is the return value of the thread
  end
end

# We wait for every thread to complete and collect their return values
results = threads.map(&:join).map(&:value)
puts "All done! Results: #{results}"

I usually recommend storing your threads in an array like I did above. It makes it much easier to manage them as a group rather than creating variables like thread1, thread2, etc.

Wait, what is the GIL and does it mean my threads are "fake"?

You'll hear the term GIL (Global Interpreter Lock) a lot in the Ruby community. Specifically, MRI (the standard version of Ruby you're likely using) has one. The GIL is essentially a lock that ensures only one thread can execute Ruby bytecode at any given moment.

Does this mean threads are useless? No, but it means you aren't getting true parallelism for CPU-heavy tasks. If you have an 8-core processor and you write a Ruby script to calculate prime numbers using 8 threads, you won't see an 8x speedup. In fact, it might even be slower because of the overhead of switching between threads.

The GIL doesn't stop "concurrency," just "parallelism." Your threads are still switching back and forth very quickly, giving the illusion of happening at once, but they are taking turns on a single CPU core.

If the GIL limits me, when should I actually use threads?

This is where most developers get confused. The GIL is only a bottleneck for CPU-bound tasks (like heavy math, image processing, or sorting huge arrays). However, for I/O-bound tasks, the GIL is practically irrelevant.

When a Ruby thread starts an I/O operation—like waiting for a response from a database, reading a file from a slow disk, or making an HTTP request—it actually releases the GIL while it waits. While Thread A is waiting for a website to respond, the GIL is free, and Thread B can jump in and start its own request.

If your app spends most of its time waiting for networks or disks, threads are your best friend. If your app spends its time crunching numbers, you should look into Processes (using fork) or a different Ruby implementation like JRuby or TruffleRuby, which don't have a GIL.

How do I stop my threads from overwriting each other's data?

When two threads try to modify the same variable at the exact same time, you get a "race condition." Even though the GIL exists, it can switch threads between any two Ruby operations. For example, count += 1 is actually three steps: read the value, add one, and write it back. A thread switch can happen right in the middle of that.

To fix this, we use a Mutex (short for Mutual Exclusion). A Mutex acts like a bathroom key: only the thread holding the key can enter the critical section of code. Everyone else has to wait in line.

# A dangerous example without a Mutex
counter = 0
lock = Mutex.new
threads = []

10.times do
  threads << Thread.new do
    1000.times do
      # Without the mutex, this would often result in a sum less than 10,000
      lock.synchronize do
        counter += 1
      end
    end
  end
end

threads.each(&:join)
puts "Final count: #{counter}" # Now it's guaranteed to be 10,000

Just a word of caution: don't wrap your entire program in a Mutex. That defeats the purpose of threading because you're effectively forcing everything back into a single-file line. Only lock the specific lines of code that touch shared data.




📋 Practical Task

Exercise: Multi-Threaded Website Availability Monitor

Build a script that checks the availability of a list of websites concurrently. Your script should meet the following requirements:

  • Define an array of at least 5 different URLs.
  • Use Thread.new to check each URL. Use Net::HTTP.get (you'll need to require 'net/http') to attempt to fetch the page.
  • Create a shared hash called results (e.g., { "google.com" => "Up", "broken-link.com" => "Down" }).
  • Because multiple threads will be writing to the results hash, you must use a Mutex to ensure the hash is updated safely.
  • Ensure the main program waits for all threads to finish before printing the final report of which sites are "Up" and which are "Down".
Rating
0 0

There are no comments for now.

to be the first to leave a comment.