Packages
Virtual time extension to GenServer and GenStateMachine allowing testing time-based actor systems orders of magnitude faster than in wallclock-time. Includes actor simulation DSL with statistics, tracing, and code generation into other Actor Model implementations in C++, Pony, Go, Rust, Java.
Retired package: Deprecated - deprecated the initial state of the library
Current section
Files
Jump to
Current section
Files
lib/dining_philosophers.ex
defmodule DiningPhilosophers do
@moduledoc """
Classic dining philosophers problem solved with actor simulation.
Five philosophers sit at a round table with five forks. Each philosopher
needs two forks to eat. This simulation demonstrates:
- Resource contention
- Synchronous communication (requesting forks)
- State machines (thinking -> hungry -> eating -> thinking)
- Deadlock-free solution using asymmetric fork acquisition
"""
@doc """
Creates a simulation of the dining philosophers problem.
Options:
- `:num_philosophers` - Number of philosophers (default: 5)
- `:think_time` - Time spent thinking in ms (default: 100)
- `:eat_time` - Time spent eating in ms (default: 50)
- `:trace` - Enable message tracing (default: true)
## Example
simulation = DiningPhilosophers.create_simulation(
num_philosophers: 5,
think_time: 100,
eat_time: 50,
trace: true
)
simulation = ActorSimulation.run(simulation, duration: 5000)
stats = ActorSimulation.get_stats(simulation)
"""
def create_simulation(opts \\ []) do
num_philosophers = Keyword.get(opts, :num_philosophers, 5)
think_time = Keyword.get(opts, :think_time, 100)
eat_time = Keyword.get(opts, :eat_time, 50)
trace = Keyword.get(opts, :trace, true)
simulation = ActorSimulation.new(trace: trace)
# Create forks
simulation = Enum.reduce(0..(num_philosophers - 1), simulation, fn i, sim ->
ActorSimulation.add_actor(sim, fork_name(i),
on_match: [
{:request, fn state ->
if state.held_by == nil do
{:reply, :granted, %{state | held_by: :philosopher}}
else
{:reply, :denied, state}
end
end},
{:release, fn state ->
{:reply, :ok, %{state | held_by: nil}}
end}
],
initial_state: %{held_by: nil}
)
end)
# Create philosophers with asymmetric fork acquisition to prevent deadlock
# Odd-numbered philosophers pick up left fork first
# Even-numbered philosophers pick up right fork first
Enum.reduce(0..(num_philosophers - 1), simulation, fn i, sim ->
{first_fork, second_fork} = if rem(i, 2) == 0 do
{fork_name(i), fork_name(rem(i + 1, num_philosophers))}
else
{fork_name(rem(i + 1, num_philosophers)), fork_name(i)}
end
philosopher_behavior = create_philosopher_behavior(first_fork, second_fork)
ActorSimulation.add_actor(sim, philosopher_name(i),
send_pattern: {:periodic, think_time, {:start_hungry, first_fork, second_fork}},
targets: [first_fork], # Will request from fork when hungry
on_receive: philosopher_behavior,
initial_state: %{
first_fork: first_fork,
second_fork: second_fork,
eat_time: eat_time,
think_time: think_time,
times_eaten: 0
}
)
end)
end
defp philosopher_name(i), do: :"philosopher_#{i}"
defp fork_name(i), do: :"fork_#{i}"
defp create_philosopher_behavior(first_fork, second_fork) do
fn msg, state ->
case msg do
{:start_hungry, ^first_fork, ^second_fork} ->
# Philosopher is hungry, request first fork
{:send, [{first_fork, {:call, :request}}], state}
_ ->
# Ignore responses for now in this simplified version
{:ok, state}
end
end
end
@doc """
Gets statistics about how many times each philosopher ate.
"""
def eating_stats(simulation) do
philosophers = [:philosopher_0, :philosopher_1, :philosopher_2, :philosopher_3, :philosopher_4]
Enum.map(philosophers, fn name ->
case Map.get(simulation.actors, name) do
nil -> {name, 0}
%{pid: pid} ->
stats = ActorSimulation.Actor.get_stats(pid)
# The times_eaten would be in the user state, but we can't easily access it
# For now, estimate from messages
{name, div(stats.sent_count, 2)} # Each eat cycle sends 2 messages
end
end)
|> Map.new()
end
end