Mutexes in Julia

In the previous example, we saw how to manage simple counter state using atomic operations. For more complex state, we can use a mutex to safely access data across multiple tasks.

using Base.Threads

# Container holds a dictionary of counters; since we want to
# update it concurrently from multiple tasks, we
# add a ReentrantLock to synchronize access.
mutable struct Container
    lock::ReentrantLock
    counters::Dict{String, Int}
end

# Lock the mutex before accessing `counters`; unlock
# it at the end of the function.
function inc!(c::Container, name::String)
    lock(c.lock) do
        c.counters[name] = get(c.counters, name, 0) + 1
    end
end

# Note that ReentrantLock is initialized in the constructor
function Container()
    Container(ReentrantLock(), Dict{String, Int}())
end

function main()
    c = Container()
    c.counters["a"] = 0
    c.counters["b"] = 0

    # This function increments a named counter
    # in a loop.
    function doIncrement(name::String, n::Int)
        for i in 1:n
            inc!(c, name)
        end
    end

    # Run several tasks concurrently; note
    # that they all access the same Container,
    # and two of them access the same counter.
    @sync begin
        @spawn doIncrement("a", 10000)
        @spawn doIncrement("a", 10000)
        @spawn doIncrement("b", 10000)
    end

    println(c.counters)
end

main()

Running the program shows that the counters updated as expected.

$ julia mutexes.jl
Dict("b" => 10000, "a" => 20000)

In this Julia version:

  1. We use ReentrantLock from Base.Threads instead of sync.Mutex.
  2. The Container struct is defined as mutable to allow modification of its fields.
  3. The inc! function uses a do block with lock to ensure thread safety.
  4. We use @spawn to create concurrent tasks instead of goroutines.
  5. @sync is used to wait for all spawned tasks to complete, similar to WaitGroup in Go.
  6. Julia’s Dict is used instead of Go’s map.

This example demonstrates how to use mutexes in Julia to safely manage state across multiple concurrent tasks.

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