RWLock (rwlock v0.1.1)

View Source

A concurrent Readers/Writer lock implementation built on GenServer.

RWLock allows multiple readers to acquire a shared (:sh_lock) lock simultaneously, while ensuring that only one writer (:ex_lock) can hold an exclusive lock at any given time.

This module manages lock ownership across processes and ensures correct coordination between readers and writers for a given resource key (referred to as on).

Features

  • Multiple readers can hold a lock concurrently (sh_lock)
  • Only one writer can hold a lock exclusively (ex_lock)
  • Fair queueing via an internal :queue structure
  • Lock ownership tracking via process identifiers
  • Automatic wake-up of waiting processes when a lock is released

Installation

The package can be installed by adding rwlock to your list of dependencies in mix.exs:

def deps do
  [
    {:rwlock, "~> 0.1.1"}
  ]
end

This library implements a RW Locking as a process that you would generally start under a supervision tree.

defmodule MyApp.Application do
  @moduledoc false

  use Application

  @impl true
  def start(_type, _args) do
    children = [
      {RWLock, name: MyApp.RWLock}
    ]

    Supervisor.start_link(children, strategy: :one_for_one)
  end
end

Example

iex> RWLock.start_link()
iex> RWLock.sh_lock(:any_term1)
:ok
iex> RWLock.sh_lock(:any_term1)
:ok
iex> RWLock.ex_lock(:any_term2)
:ok
iex> RWLock.unlock(:any_term1)
:ok
iex> RWLock.unlock(:any_term1)
:ok
iex> RWLock.unlock(:any_term2)
:ok

Lock Structure

Internally, each lock entry is a map with these keys:

%{
  locked?: boolean(),
  type: :ex | :sh | nil,
  readers: MapSet.t(),
  wlist: :queue.t(),
  on: any()
}

This structure is maintained per on key in the GenServer’s state.

Summary

Types

A process identifier that holds a lock

The lock state for a single resource

Lock type: shared (readers) or exclusive (writer)

The GenServer state holding all resource locks

Wait queue containing pending lock requests

Functions

Returns a specification to start this module under a supervisor.

Acquires an exclusive (writer) lock on a resource.

Acquires a shared (reader) lock on a resource.

Starts the RWLock GenServer process.

Unlocks a resource previously locked by the caller process.

Types

client_pid()

(since 0.1.0)
@type client_pid() :: pid()

A process identifier that holds a lock

lock()

(since 0.1.0)
@type lock() :: %{
  locked?: boolean(),
  type: lock_type(),
  readers: MapSet.t(client_pid()) | nil,
  wlist: wait_queue(),
  on: any()
}

The lock state for a single resource

lock_type()

(since 0.1.0)
@type lock_type() :: :sh | :ex | nil

Lock type: shared (readers) or exclusive (writer)

state()

(since 0.1.0)
@type state() :: %{optional(any()) => lock()}

The GenServer state holding all resource locks

wait_queue()

(since 0.1.0)
@type wait_queue() :: :queue.queue({:sh_lock | :ex_lock, GenServer.from()})

Wait queue containing pending lock requests

Functions

child_spec(init_arg)

(since 0.1.0)

Returns a specification to start this module under a supervisor.

See Supervisor.

ex_lock(on)

(since 0.1.0)
@spec ex_lock(any()) :: :ok

Acquires an exclusive (writer) lock on a resource.

The call blocks until the lock becomes available.

sh_lock(on)

(since 0.1.0)
@spec sh_lock(any()) :: :ok

Acquires a shared (reader) lock on a resource.

Multiple readers may hold the same lock concurrently, provided that no writer is active or waiting.

start_link(init_state \\ nil)

(since 0.1.0)
@spec start_link(map() | nil) :: GenServer.on_start()

Starts the RWLock GenServer process.

Optionally accepts an initial state map (usually empty).

unlock(on)

(since 0.1.0)
@spec unlock(any()) :: :ok

Unlocks a resource previously locked by the caller process.

This will trigger giving the lock to the next waiting reader(s) or writer.