やわらかテック

興味のあること。業務を通して得られた発見。個人的に試してみたことをアウトプットしています🍵

Elixirでプロセスをspawnで生成してErlangの:random.uniformを呼び出すと全て同じ値になる

何が起こったのか

まず、プロセスを複数立ち上げて並行で処理をしようと考えた。その処理の過程の中でErlang:random.uniform()(ElixirからErlangのモジュール関数を呼び出す際には:をつける)を呼び出して実行してみると、何と全て同じ値になっているではないか!!

ランダムに生成したidをPOST経由で送信した際にuserが1人しか生成されていないので不思議に思って、デバッグをしている際に気づいた
以下は、問題を再現するために記述したコードになる

Enum.map(1..10, fn num ->
  spawn(fn ->
    num = :random.uniform()
    IO.puts(num)
  end)
end)

このコードをiex起動して実行してみると、全て同じ値が出力されることが確認できる

[#PID<0.722.0>, #PID<0.723.0>, #PID<0.724.0>,
 #PID<0.725.0>, #PID<0.726.0>, #PID<0.727.0>,
 #PID<0.728.0>, #PID<0.729.0>, #PID<0.730.0>,
 #PID<0.731.0>]
0.4435846174457203
0.4435846174457203
:
:
0.4435846174457203
0.4435846174457203

なぜ〜

Erlangの公式ドキュメントをふと読んでみることに

答えは全てそこにあった。公式 is GOD
以下は引用になります

Data Types
ran() = {integer(), integer(), integer()}
The state.

uniform() -> float()
Returns a random float uniformly distributed between 0.0 and 1.0, updating the state in the process dictionary.

ここから分かることは、ランダム値の生成にprocess dictionaryに保存されているData Typesの値を利用しているということだ
つまり、このstateの値(seed値)が全てのプロセスで重複してしまっているために、同じ値が生成されているのではないかと考えられる

さらにページ下部から決定的な記述を発見したので引用

Some of the functions use the process dictionary variable random_seed to remember the current seed.
If a process calls uniform/0 or uniform/1 without setting a seed first, seed/0 is called automatically.

ふむふむ、要は:random.uniform()を呼び出した際に、最初にseed値を設定するために、seed/0を内部的に呼び出しているようで、その際にseed値をセットしているもしくは、process dictionaryからseed値を読みこんでいると考えることが出来る

そうであれば、同じ値が生成されるという現象を説明することが出来る
Erlangのコードは読み慣れていないが、該当する部分のコードを発見できたので確認しておこう

-spec uniform() -> float().

uniform() ->
    {A1, A2, A3} = case get(random_seed) of
               undefined -> seed0();  %% ここでseed0()を呼び出している!! 
               Tuple -> Tuple
           end,
    B1 = (A1*171) rem ?PRIME1,
    B2 = (A2*172) rem ?PRIME2,
    B3 = (A3*170) rem ?PRIME3,
    put(random_seed, {B1,B2,B3}),
    R = B1/?PRIME1 + B2/?PRIME2 + B3/?PRIME3,
    R - trunc(R).

なるほど、やはりそういうことだね
ということは別プロセスを立ち上げた際にこのseed値を変えてあげれば良さそう

清流elixir常連参加のりきくんにも助けて貰いました。圧倒的感謝ッ!!


問題を解決したコード

ということで以下のコードに変更したところ、無事にそれぞれのプロセスで異なる値が生成されるのを確認できた seed値の設定値については気にしてはいない。まずは結果が変化することを確認することが優先だ

base_num = :random.uniform(1000)
Enum.map(1..10, fn num ->
  spawn(fn ->
    :random.seed(num+base_num, num+base_num, num+base_num)
    num = :random.uniform()
    IO.puts(num)
  end)
end)
[#PID<0.766.0>, #PID<0.767.0>, #PID<0.768.0>,
 #PID<0.769.0>, #PID<0.770.0>, #PID<0.771.0>,
 #PID<0.772.0>, #PID<0.773.0>, #PID<0.774.0>,
 #PID<0.775.0>]
0.7457427656808528
0.7626736718805098
:
:
0.8811900152781076
0.8981209214777643

ヨシ!

おまけ。Erlangの関数を使用せずにランダム値を生成する

面倒なのでEnum.random()を使って乱数を擬似的に作成してあげれば良い
基本形は以下の通り。rangeもenumerableなので記述するのが楽だ

iex> Enum.random(1..100)
47

少数にしたい場合は任意の10n(希望の桁数)で割ってやれば良い感じになる

iex> Enum.random(1000..1000000) / 1000000
0.534401