Ruby
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Methods and Blocks
-
Section 4: Object-Oriented Ruby
-
Section 5: Metaprogramming
-
Section 6: Working with Data and Files
-
Section 7: Ruby Frameworks Overview
-
Section 8: Ecosystem and Testing
-
Section 9: Practical Projects
-
Section 10: Interview Practice
-
Section 11: Data Structures and Algorithms in Ruby
-
Section 12: More Practice Exercises
-
Section 13: Enumerable and Functional Style
-
Section 14: More OOP Practice
-
Section 15: More Testing
-
Section 16: Enumerable and Comparable Modules In Depth
-
Section 17: Ruby Standard Library: Core Utilities
-
Section 18: Ruby Standard Library: Data and Security
-
Section 19: Ruby Standard Library: CLI and Text
-
Section 20: Ruby Networking
-
Section 21: Ruby on Rails Deep Dive
-
Section 22: Ruby Metaprogramming Deep Dive
-
Section 23: Ruby Design Patterns
-
Section 24: Ruby Concurrency
-
Section 25: Ruby Testing Deep Dive
-
Section 26: Ruby Gems and Packaging
-
Section 27: Ruby Performance
-
Section 28: More Practice Exercises
-
Section 29: More Interview Practice
-
Section 30: Rails API Development
-
Section 31: Rails Authentication and Authorization
-
Section 32: Rails Testing Deep Dive
-
Section 33: Rails Performance
-
Section 34: Rails Deployment
-
Section 35: Sinatra and Lightweight Ruby Web Apps
-
Section 36: More Ruby Language Deep Dive
-
Section 37: Ruby 3.x Modern Features
-
Section 38: More Data Structures in Ruby
-
Section 39: More Practical Projects
-
Section 40: Ruby Ecosystem Tools
-
Section 41: More Practice and Review
-
Section 42: Final Practice and Mastery
-
Section 43: Ruby Interview Deep Dive
-
Section 44: Ruby Background Processing Deep Dive
-
Section 45: Ruby GraphQL
-
Section 46: Ruby Object Model Deep Dive
-
Section 47: Ruby Hanami Framework Overview
-
Section 48: Ruby gRPC and Protocol Buffers
-
Section 49: Ruby Data Processing
-
Section 50: Ruby Search Integration
-
Section 51: Ruby File Upload and Media
-
Section 52: Ruby Email and Notifications
-
Section 53: Ruby Admin Panels
-
Section 54: Ruby Feature Flags and Experimentation
-
Section 55: Ruby Monitoring and Observability
-
Section 56: Ruby Docker and Deployment Deep Dive
-
Section 57: Ruby Security Deep Dive
-
Section 58: More Advanced Metaprogramming
-
Section 59: More Final Projects
98: Threads in Ruby and the GIL
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.newto check each URL. UseNet::HTTP.get(you'll need torequire '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
resultshash, you must use aMutexto 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".
There are no comments for now.