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
101: Mutex and Thread Synchronization
Imagine you're at a coffee shop with a single, small restroom. There is one key attached to a heavy piece of wood. To use the restroom, you have to take the key, go inside, and lock the door. If another customer wants to use it while you're in there, they can't just barge in—they have to wait by the counter until you come out and hand the key back. The key is the "token" that grants exclusive access. Without that system, you'd have a very awkward and chaotic morning.
In Ruby, a Mutex (short for "mutual exclusion") is that key. When you have multiple threads running, they all share the same memory space. If two threads try to modify the same variable at the exact same millisecond, they can trip over each other. This is what we call a race condition. One thread might read a value, but before it can update it, another thread jumps in and changes it, leaving the first thread to overwrite the second one's work with stale data.
Where the race condition happens
I want to show you a classic example. Let's say we're building a simple bank account. You'd think adding money to a balance is a simple operation, but at the machine level, balance += 10 is actually three steps: read the balance, add ten to it, and write the result back. If two threads do this simultaneously, one update often just disappears.
account_balance = 0
threads = []
10.times do
threads << Thread.new do
1000.times { account_balance += 1 }
end
end
threads.each(&:join)
puts "Final balance: #{account_balance}"
# You'd expect 10,000, but you'll likely get something like 8432.
Because there's no "key" to the account balance, the threads are fighting over it. They are reading the same initial value and writing back the same result, effectively erasing each other's progress.
Putting a lock on the balance
To fix this, we introduce a Mutex. I always recommend using the synchronize method rather than calling lock and unlock manually. Why? Because if your code raises an error while you hold a manual lock, the lock stays locked forever, and your entire program freezes. synchronize handles the unlocking for you, even if things go south inside the block.
balance_mutex = Thread::Mutex.new
account_balance = 0
threads = []
10.times do
threads << Thread.new do
1000.times do
balance_mutex.synchronize do
account_balance += 1
end
end
end
end
threads.each(&:join)
puts "Final balance: #{account_balance}"
# Now it will be exactly 10,000 every single time.
Notice how we wrapped the critical section—the part where the shared data is modified—inside the synchronize block. Now, when Thread A enters that block, it "takes the key." If Thread B reaches that line, it simply pauses and waits until Thread A finishes and "returns the key."
The cost of safety
Now, a word of caution: don't wrap your entire program in a Mutex. I've seen developers do this because they're afraid of race conditions, but it completely defeats the purpose of using threads. If only one thread can do anything at a time, you've essentially turned your multi-threaded program back into a single-threaded one, but with the added overhead of managing locks.
The goal is to keep the synchronized block as small as possible. Only lock the specific line or two where the shared state is actually being mutated. If you're doing a heavy API call or a slow database query, do that outside the mutex, then enter the mutex only to save the result to your shared variable.
📋 Practical Task
The Thread-Safe Concert Ticket Booth
You are building a ticket booking system for a popular concert. There are only 50 tickets available. You have 10 different threads (representing 10 different users) all trying to buy as many tickets as they can at the same time.
Your Goal: Create a script where multiple threads attempt to decrement a tickets_available variable. You must use a Mutex to ensure that the tickets never drop below zero (no overselling) and that the final count is accurate.
Requirements:
- Start with
tickets_available = 50. - Create 10 threads.
- Each thread should try to buy 10 tickets in a loop.
- Inside the loop, the thread must check if tickets are available and then decrement the count.
- Use a
Mutexto ensure that the "check and decrement" happens atomically. - Print the final number of tickets remaining (which should be 0) and ensure no "negative tickets" were sold.
There are no comments for now.