1. Technology
You can opt-out at any time. Please refer to our privacy policy for contact information.

Mutexes

By

A "mutex" is a mutual exclusion lock. In the previous thread pool example there was a race condition. A race condition is when more than one thread tries to access the same resource at the same time, corrupting the state of that resource. If any thread accesses a common variable (for example, the jobs array) it must lock it first with a mutex.

A mutex is like a "occupied" sign on a changing room door. When one thread goes it, it puts the occupied sign up. When it leaves, it puts it back down. When another thread comes to the door and sees the occupied sign, it must wait until the other thread is finished and puts the sign back down. And, in order to prevent another race condition in putting the sign up and down (since the sign itself is a shared resource, it too can be vulnerable to race conditions), you ask the operating system to do it for you. Acquiring a mutex is what's called an "atomic operation," it can't be interrupted. So just remember, acquire the mutex before touching any shared resource and make a copy of any data from the resource, don't share anything without a mutex. You have to be very careful in threaded programs, since they're all running in the same address space, it's quite easy to accidentally corrupt the state of the entire program (or at least of the other threads).

The following example shows the easiest way to use mutexes: the synchronize method. It adds a new variable from the previous thread pool example, as well as a call to synchronize before accessing the jobs array.


#!/usr/bin/env ruby
require 'thread'
require 'pp'

jobs = Array.new(20).map{ rand(10) }
jobs_mutex = Mutex.new
max_threads = 3
threads = []

puts "Jobs:"
pp jobs

until jobs.empty? && threads.empty?
  until jobs.empty? || threads.size == max_threads
    threads << Thread.new do
      job = jobs_mutex.synchronize do
        jobs.pop
      end

      puts "Sleeping #{job} seconds"
      sleep job
    end
  end

  sleep 1

  threads.each do|t|
    t.join unless t.status
  end

  threads.delete_if{|t| not t.status }
end

The first change here is jobs_mutex = Mutex.new. Each resource you need to lock needs to have a mutex. The mutex isn't tied to the resources in any way, only by convention. So if there were several variables related to the jobs array, this single mutex would still cover them. Remember, this is all based on the honor system. Any threads can overstep its bounds at any time, so check your code carefully and be sure it's clear what each mutex is supposed to protect. Mutexes aren't very complicated things, they're usually just created as they were here and only used with the synchronize method.

The synchronize method itself is a shortcut method. It first locks the mutex. Remember, don't check to see if the mutex is locked first, just lock it and let the OS sort it out. The synchronize method then runs the block, and when the block is finished, unlocks the mutex. The result of the block is returned, and in this case stored in the job variable.

You can manually do this process as well, but it's usually not worth the effort. The only time where this may be needed is if the mutex is already locked and you'd like to do something else while it's locked. For example, if the jobs mutex is locked you might want to check on the thread pool one more time first. In that case, you can use the Mutex#locked?, Mutex#lock and Mutex#unlock methods. Just remember the golden rule of mutexes: just call lock and let the OS sort it out. Don't check to see if it's unlocked and use the jobs array "really quick." Another thread could be scheduled to run at that same exact time and this will end badly. Remember that although you may write your program so such collisions would be exceedingly rare, a one in a million chance is still a chance and with programs running so quickly on modern computers, a one in a million chance will still happen. So lock it before you use it!

Another useful method is the try_lock method, which avoids the situation above. The try_lock method tries to lock the mutex just like lock. If the method succeeds in locking the mutex, it returns true. Otherwise, it immediately returns false instead of waiting for the mutex to become available.

However, in my attempt to make a simple example that couldn't possibly go wrong… I made a mistake. Can you spot the race conditions still lurking in this program? While the jobs array is only written to from within mutex protected code, both loops check if it's empty from outside the mutex. So what if one thread pops the last remaining job from the list after the loop checks if it's empty but before the loop acts on it? The loop will try to spawn a new thread with an empty job (since if you pop from an empty array you get nil). This just goes to show just how tricky threaded programming can be, and why it's avoided at all costs. The inner and outer loops will need to be restructured to lock the mutex before querying the state of the jobs array.

  1. About.com
  2. Technology
  3. Ruby
  4. Advanced Ruby
  5. Mutexes

©2014 About.com. All rights reserved.