自己紹介
こんにちは、@mnishiguchi です。
仕事や趣味でWeb アプリケーションや組み込みシステムの開発に取り組んでいます。
まだまだ知らないことばかりで、日々學びの連続です。
Qiita では、Elixir や Nerves を中心に、ちょっとした氣づきや學びを記録しています。
はじめに
Elixir でプログラミングをしていると、異なるプロセス間や、同一プロセス内でも異なるタイミングで取得する必要がある状態を共有・保存したい場面があります。
そのような場合、Elixir には外部サービスや追加ライブラリを使わずに利用できるインメモリストレージが標準で用意されています。
ここでは代表的なものをご紹介します。
モジュール | 主な用途 |
---|---|
Agent | 単純な共有状態やキャッシュの保持 |
GenServer | 状態管理とカスタムロジックの実装 |
:ets | 大規模データや共有テーブルの高速アクセス |
:persistent_term | 読み取り中心でほぼ固定のグローバル設定値 |
:counters | 高頻度で更新する数値カウンター |
Process | プロセス内だけで参照できるメタ情報 |
今回使用した Erlang/Elixir のバージョンは以下のとおりです。
Erlang/OTP 28
Elixir 1.18.4
Agent
Agent は 単純な状態の取得・更新に特化したプロセスです。
例
Map のように値の追加・更新・削除が行える最小限のモジュールを用意します。
defmodule MyKvAgent do
use Agent
@name __MODULE__
def start_link(_opts \\ []) do
initial_state = %{}
Agent.start_link(fn -> initial_state end, name: @name)
end
def get(key, default \\ nil) do
Agent.get(@name, &Map.get(&1, key, default))
end
def put(key, value) do
Agent.update(@name, &Map.put(&1, key, value))
end
def delete(key) do
Agent.update(@name, &Map.delete(&1, key))
end
end
# プロセスを開始
MyKvAgent.start_link()
# 値を追加
MyKvAgent.put(:hogehoge, "元氣")
# 値を取得
MyKvAgent.get(:hogehoge)
:sys.get_state/1で現在の内容を確認することができますが、これはデバッグ専用です。
:sys.get_state(MyKvAgent)
GenServer
GenServer は、状態と処理ロジックを一つのプロセスで扱える OTP の基本機能です。状態の読み書きだけでなく、任意のメッセージ処理や非同期処理にも対応できます。
例
Map のように値の追加・更新・削除が行える最小限のモジュールを用意します。
defmodule MyKvServer do
use GenServer
@name __MODULE__
## クライアントAPI
def start_link(_opts \\ []) do
init_args = nil
GenServer.start_link(__MODULE__, init_args, name: @name)
end
def get(key, default \\ nil) do
GenServer.call(@name, {:get, key, default})
end
def put(key, value) do
GenServer.call(@name, {:put, key, value})
end
def delete(key) do
GenServer.call(@name, {:delete, key})
end
## サーバコールバック
@impl true
def init(_args) do
initial_state = %{}
{:ok, initial_state}
end
@impl true
def handle_call({:get, key, default}, _from, state) do
{:reply, Map.get(state, key, default), state}
end
@impl true
def handle_call({:put, key, value}, _from, state) do
{:reply, :ok, Map.put(state, key, value)}
end
@impl true
def handle_call({:delete, key}, _from, state) do
{:reply, :ok, Map.delete(state, key)}
end
end
# プロセスを起動
MyKvServer.start_link()
# 値を追加
MyKvServer.put(:hogehoge, "元氣")
# 値を取得
MyKvServer.get(:hogehoge)
:sys.get_state/1で現在の内容を確認することができますが、これはデバッグ専用です。
:sys.get_state(MyKvServer)
:ets (Erlang Term Storage)
:ets は 高速で並列アクセス可能なインメモリテーブルです。大きなデータセットの共有や頻繁な読み書きに適しています。
例
Map のように値の追加・更新・削除が行える最小限のモジュールを用意します。
defmodule MyKvEts do
@table __MODULE__
def init do
case :ets.whereis(@table) do
:undefined ->
ref = :ets.new(@table, [:set, :named_table, :public])
{:ok, ref}
ref when is_reference(ref) ->
{:ok, ref}
end
end
def get(key, default \\ nil) do
case :ets.lookup(@table, key) do
[] -> default
[{^key, value}] -> value
end
end
def put(key, value) do
:ets.insert(@table, {key, value})
:ok
end
def delete(key) do
:ets.delete(@table, key)
:ok
end
end
# テーブルを初期化
MyKvEts.init()
# 値を追加
MyKvEts.put(:hogehoge, "元氣")
# 値を取得
MyKvEts.get(:hogehoge)
:ets.tab2list/1でetsテーブルの中身を見ることができます。
:ets.tab2list(MyKvEts)
:ets.info/1でetsテーブルの設定や状態を確認できます。
:ets.info(MyKvEts)
:ets.all/0で現在のノードのすべてのテーブルを列挙できます。色んな所でetsが利用されているのが見えてたいへん興味深いです。
:ets.all()
:persistent_term
:persistent_term は、アプリケーション全体から参照できる読み取り特化型のストレージです。
読み込みは非常に高速ですが、更新や削除を行うと全プロセスでGC(ガーベジコレクション)が発生するため高コストです。
そのため、ほぼ固定の設定値やルックアップテーブルなど、頻繁に参照するが、ほとんど更新しないデータの保存に適しています。
例
:persistent_term は、特に初期化は不要です。
# 値を設定
:persistent_term.put(:hogehoge, "元氣")
# 値を取得
:persistent_term.get(:hogehoge)
:counters
:counters は、複数プロセスから効率的に同時更新できる数値カウンターです。
加算・減算・取得が高速で、整数インデックス(1始まり)で管理します。
高頻度更新が必要なメトリクスやレート制限、集計処理に適しています。
例
初期化の際にカウンターの数を指定します。
インデックス番号で複数のカウンターを管理します。
# 初期化
counters_ref = :counters.new(_how_many = 2, _options = [])
# カウンター1に1を足す
index = 1
:counters.add(counters_ref, index, 1)
:counters.get(counters_ref, index)
# カウンター2から5引く
index = 2
:counters.sub(counters_ref, index, 5)
:counters.get(counters_ref, index)
プロセス辞書
実はプロセス自体にもプロセス辞書(Process Dictionary) という内部データを保持する仕組みがあります。
プロセス内でProcess.put/2 と Process.get/1 で操作ができますが、他のプロセスからは見えません。プロセスが終了するとデータも消えます。
例
# 値を保存
Process.put(:hogehoge, "元氣")
# 値を取得
Process.get(:hogehoge)
Process.info/1で現在の内容を確認することができますが、これはデバッグ専用です。
for {k, v} <- Process.info(self())[:dictionary], do: {k, inspect(v)}
おわりに
Elixir の標準機能だけで、これだけ多彩なインメモリストレージが揃っています。これらを適材適所で、どんどん活用していきましょう。