Mutexes in Crystal

Here’s the translated code and explanation in Crystal, formatted for Hugo:

In this example, we’ll explore how to use mutexes to safely access data across multiple fibers. This is useful for more complex state management than what atomic operations can provide.

require "mutex"

# Container holds a hash of counters; since we want to
# update it concurrently from multiple fibers, we
# add a Mutex to synchronize access.
class Container
  @mu : Mutex
  @counters : Hash(String, Int32)

  def initialize
    @mu = Mutex.new
    @counters = {"a" => 0, "b" => 0}
  end

  # Lock the mutex before accessing @counters; unlock
  # it at the end of the method.
  def inc(name : String)
    @mu.synchronize do
      @counters[name] += 1
    end
  end

  def counters
    @counters
  end
end

# This function increments a named counter
# in a loop.
def do_increment(c : Container, name : String, n : Int32, channel : Channel(Nil))
  n.times do
    c.inc(name)
  end
  channel.send(nil)
end

def main
  c = Container.new

  # Run several fibers concurrently; note
  # that they all access the same Container,
  # and two of them access the same counter.
  channel = Channel(Nil).new
  spawn { do_increment(c, "a", 10000, channel) }
  spawn { do_increment(c, "a", 10000, channel) }
  spawn { do_increment(c, "b", 10000, channel) }

  # Wait for the fibers to finish
  3.times { channel.receive }

  puts c.counters
end

main

Running the program shows that the counters are updated as expected:

$ crystal run mutexes.cr
{"a" => 20000, "b" => 10000}

In this Crystal version, we’ve made a few adjustments to match the language’s idioms:

  1. We use require "mutex" to import the mutex functionality.

  2. The Container class is defined with instance variables @mu and @counters.

  3. The inc method uses the synchronize method of the mutex, which automatically handles locking and unlocking.

  4. We use fibers (Crystal’s lightweight concurrency primitive) instead of goroutines.

  5. We use a Channel to synchronize the fibers, similar to how WaitGroup is used in the original example.

  6. The do_increment function is defined at the top level and takes the container, name, count, and channel as parameters.

  7. We use spawn to create new fibers.

This example demonstrates how to use mutexes in Crystal to safely manage shared state across multiple concurrent fibers. The mutex ensures that only one fiber can access or modify the shared counters at a time, preventing race conditions.

Next, we’ll look at implementing this same state management task using only fibers and channels.