Mutex
is a lightweight mutex or "locks" implementation that fits under your
supervision tree and allows processes to work on shared ressources one by one.
This can be a simple alternative to job queues or filesystem transactions and allows to limit resource usage on a server.
Also, Mutex
supports locking multiple keys without deadlocks.
- Documentation
- Installation
- Basic usage
- Error Handling
- Avoiding Deadlocks
- Lock handover
- Name registration
- Metadata (Removed)
- Copyright and License
The documentation is hosted on Hexdocs.
This package can be installed by adding :mutex
to your list of dependencies in
mix.exs
:
def deps do
[
{:mutex, "~> 3.0"},
]
end
This library implements a mutex as a process that you would generally start under a supervision tree.
See start_link/1 for options.
defmodule MyApp.Application do
@moduledoc false
use Application
@impl true
def start(_type, _args) do
children = [
{Mutex, name: MyApp.Mutex}
]
Supervisor.start_link(children, strategy: :one_for_one)
end
end
Now from anywhere in the application code you can lock access to a a resource with the guarantee that no concurrent execution of that code can be executed.
Of course, this requires that all code attempting to access that resource uses the mutex as well.
lock = Mutex.await(MyApp.Mutex, :file_manager)
do_something_with_the_filesystem(arg)
:ok = Mutex.release(MyApp.Mutex, lock)
To automatically lock and release the key, use Mutex.with_lock/3
:
Mutex.with_lock(MyApp.Mutex, :file_manager, fn ->
do_something_with_the_filesystem(arg)
end)
Here is a more complete example of working with storing data in a file:
path = "/tmp/counter-#{System.system_time(:microsecond)}"
File.write!(path, "0")
tasks =
for _ <- 1..10 do
Task.async(fn ->
Mutex.with_lock(MyApp.Mutex, :file_manager, fn ->
counter = String.to_integer(File.read!(path))
File.write!(path, Integer.to_string(counter + 1))
end)
end)
end
Enum.each(tasks, &Task.await/1)
counter = String.to_integer(File.read!(path))
IO.puts("Total count is: #{counter}")
This will output:
Total count is: 10
The same code but without the mutex will generally give a final value of 1
.
Whenever a process that locked a key on the mutex crashes, the mutex automatically unlocks the key so any other process can lock it in its turn.
But if you catch exceptions you may forget to release the keys and keep unnecessary keys locked for a while :
# Do not do this
try do
lock = Mutex.await(MyApp.Mutex, :some_key)
throw(:fail)
# This will never be called:
Mutex.release(MyApp.Mutex, lock)
catch
:throw, :fail -> :ok
end
Whenever possible, avoid to lock keys in try
, if
, for
, ... blocks.
When using Mutex.with_lock/3
, the lock will be automatically released if the
given fun raises, throws, or exits.
Exceptions and thrown values are reraised and rethrown so you still have to handle them.
# Do this instead
try do
Mutex.with_lock(MyApp.Mutex, :some_key, fn ->
throw(:fail)
end)
catch
:throw, :fail -> :ok
end
A multilock version is also available with Mutex.with_lock_all/3
.
Both functions can accept a fun of arity 1
that will be given the lock as its
argument.
A deadlock would occur if multiple processes would attempt to lock the same keys in a different order:
# Do not do this
def handle_order(buyer, seller) do
lock1 = Mutex.await(MyApp.Mutex, buyer)
lock2 = Mutex.await(MyApp.Mutex, seller)
do_some_work_with_users(buyer, seller)
Mutex.release(MyApp.Mutex, lock1)
Mutex.release(MyApp.Mutex, lock2)
end
spawn(fn -> handler_order(:user_1, :user_2) end) # Process 1
spawn(fn -> handler_order(:user_2, :user_1) end) # Process 2
Process 1 will first lock :user_1
and process 2 will lock :user_2
, and then
each process is waiting for the key that is already locked by the other one.
If any process should have, at any given time, several keys locked, those keys shall have been locked all at once.
This simple rule is enough to be free from deadlocks, and Mutex.await_all/2
is
the simplest way to respect that rule.
# Do this instead
def handle_order(buyer, seller) do
lock = Mutex.await_all(MyApp.Mutex, [buyer, seller])
do_some_work_with_users(buyer, seller)
Mutex.release(MyApp.Mutex, lock)
end
If you really have to lock keys in a loop, or at different places, the
Mutex.goodbye/1
function allows to simply release all the keys locked by the
calling process at once.
It is possible to transfer ownership of a lock to another process with the
Mutex.give_away/4
function.
This is useful if you need to check that a resource is available and then start a long running process that needs the lock and terminate quickly.
For instance, we want to have one instance only of a video encoding process in our application. Video encoding is started from a web controller that needs to send an HTTP response in a timely fashion.
def handle_request(encoding_request) do
case Mutex.lock(MyApp.Mutex, :encoding_server) do
{:error, :busy} ->
"encoding not available"
{:ok, lock} ->
{:ok, task_pid} =
Task.Supervisor.start_child(
Encoding.Supervisor,
&encode_video/0
)
# Here we pass the video path as the "gift data" but in the real world
# that should be given as an argument to the task function.
:ok = Mutex.give_away(MyApp.Mutex, lock, task_pid, encoding_request.path)
"encoding started"
end
end
def encode_video do
receive do
{:"MUTEX-TRANSFER", _, lock, video_path} ->
Encoder.encode_video(video_path)
Mutex.release(MyApp.Mutex, lock)
Encoder.cleanup(video_path)
after
1000 -> exit(:no_lock_received)
end
end
Processes having a key locked in a Mutex
can be called using a :via
tuple with GenServer.call/3
or GenServer.cast/2
.
The :via
tuple is built as follows:
{
:via,
Mutex,
{<mutex_name_or_pid>, <key>}
}
Example calling a GenServer
process with such tuples:
{:ok, _} = Mutex.start_link(name: MyApp.Mutex)
defmodule MyGenServer do
def init(_), do: Mutex.lock(MyApp.Mutex, :some_key)
def handle_call(:greetings, _, state), do: {:reply, :hello, state}
end
{:ok, _} = GenServer.start_link(MyGenServer, [])
:hello = GenServer.call({:via, Mutex, {MyApp.Mutex, :some_key}}, :greetings)
This lets you check if a key is currently locked in the Mutex:
# Returns #PID<…> or nil
GenServer.whereis({:via, Mutex, {MyApp.Mutex, :some_key}})
As any other name registry, you can directly set the name of your process in GenServer.start_link/3
:
GenServer.start_link(MyGenServer, [], name: {:via, Mutex, {MyApp.Mutex, :some_key}})
In this case the lock is automatically taken by the GenServer
process.
Important, this mechanism is intended for use with mutexes handling a small amount of processes working together. Do not use name registration with a single mutex in you application or you could get bottlenecks. The Registry module is intended for such uses and supports unique keys and partitionning registered keys for performance.
A Mutex process could carry metadata attached to it and would send it to any process acquiring a lock. This has been removed as it was confusing. Metadata was not tied to a given key but to the mutex itself.
To replace this functionality, you may use a Registry
, an ETS table,
:persistent_term
or other shared data mechanisms provided by the Elixir and
Erlang platforms.
Copyright (c) 2017, Ludovic Demblans
This work is free. You can redistribute it and/or modify it under the terms of the MIT License. See the LICENSE.md file for more details.