Skip to Content
Course content

101: Mutex and Thread Synchronization

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

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 Mutex to 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.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.