Ruby mutex vs. GIL

Viewed 334

Does a Ruby mutex allow two threads to execute concurrently if one thread is waiting on blocking I/O?

This is my understanding of how the GIL works for MRI. I'm curious if there's any difference between a mutex and GIL?

2 Answers

Yes, this works. Only because of this, using threads in MRI is actually useful for many workloads, even though only a single thread can concurrently execute "code".

A common example is a web application such as a Rails app. Here, you can run multiple threads with e.g. Puma in a single process, each handling a single request. Since you often wait for the database here, a different thread can also execute. This works because the database adapter (e.g. mysql2 or pg) releases the GIL with a call to the database and re-acquires it once the reply arrives and is delivered up to the caller.

With a Mutex however, you are ensuring that a certain block of code is only executed by a single thread at a time. A common example is an adder:

class Adder
  attr_reader :number

  def initialize
    @number = 0
  end

  def add(number)
    new_number = @number
    new_number = new_number + number
    @number = new_number

    # The above code is extremely verbose to show what's happening here.
    # It is equivalent to
    # @number += number
  end
end

Here, the Adder#add method is not thread save. If multiple threads try to concurrently add numbers, some updates will be lost since the operation is not atomic (but consists of a read, an operation and a write). With a Mutex around add, you can ensure that the operation finishes in one thread and the shared data structures are updated consistently.

As a general advice, you should always use a Mutex if you are reading or updating any data shared across thread boundaries. To ensure correctness, you should also strictly control which data structures are passed across thread boundaries and avoid it if possible.

If you still need this, the concurrent-ruby gem provides some thread-safe data structures which can help with sharing data across thread-boundaries.

The Global Interpreter Lock on the default implementation (MRI) prevents any two Ruby threads from running concurrently. A Mutex prevents specific threads from running concurrently, where others are free to do whatever.

Note that the rules are different in implementations that do not have a GIL, like JRuby, where threads can run independently and concurrently.

In effect the Global Interpreter Lock is itself a mutex, but it's one that's engaged by default, not explicitly in your code, like with synchronize.

If a thread is blocked waiting on a mutex then other threads continue to run normally, they're able to do whatever they need independently.

The Global Interpreter Lock is also different in that your Ruby thread will be periodically interrupted so that other threads can run. This is to prevent one thread from monopolizing the lock.

Related