Surprising output using Parallel gem with srand and rand
I'm working with ruby 2.4.1 + parallel 1.11.2. I'm running the following in irb:
require 'parallel'
srand(1)
Parallel.map([0, 1], in_processes: 2) { |i| puts "in process #{i}; rand => #{rand}" }
My understanding is that when in_processes
is specified, Parallel.map
forks the process and then executes the loop body. Given that, I expect both processes to have the same global state and therefore I had expected that both would output the same random number. However, here's what I get:
irb(main):003:0> Parallel.map([0, 1], in_processes: 2) { |i| puts "in process #{i}; rand => #{rand}" }
in process 1; rand => 0.48721687007281356
in process 0; rand => 0.7502824863668285
=> [nil, nil]
For the record, if I execute srand(1)
and then rand
, I get 0.417022004702574, so it appears that neither process got the random number seed which I set. I can get the behavior I want by setting the random number seed within the loop, but before I do that I'm trying to understand why it doesn't work to put the seed outside the loop.
I'm trying to make sense of this situation. Is this behavior somehow specific to the random number generator, so I wouldn't necessarily have the same problem (namely, expected shared initial state and didn't get it) with other objects? Or is Parallel not really having the same effect as a plain fork
system call?
The documentation about Parallel with in_processes
led me to believe that it acted like fork
, but that doesn't seem to be the case here, hence my surprise.
EDIT: Some more experimenting shows that the same behavior arises when using Process.fork
, so the problem has to do with fork
and not the Parallel gem.
$ cat foo.rb
srand(1)
pid = Process.fork
if !pid
then puts "child says rand => #{rand}"
else puts "parent says rand => #{rand}"
Process.wait(pid)
end
$ ruby foo.rb
parent says rand => 0.417022004702574
child says rand => 0.7054895237863591
EDIT: Further investigation seems to show that the option isolation: true
is relevant here. When accessing a variable in the parent process, isolation: true
seems to have the desired effect.
irb(main):037:0> foo = 1;
irb(main):038:0* Parallel.map([0, 1, 2, 3, 4, 5], in_processes: 2) { |i| puts "in process #{i}; foo = #{foo}"; foo = foo + 1 }
in process 0; foo = 1
in process 2; foo = 2
in process 3; foo = 3
in process 4; foo = 4
in process 5; foo = 5
in process 1; foo = 1
=> [2, 2, 3, 4, 5, 6]
irb(main):039:0> foo = 1;
irb(main):040:0* Parallel.map([0, 1, 2, 3, 4, 5], in_processes: 2, isolation: true) { |i| puts "in process #{i}; foo = #{foo}"; foo = foo + 1 }
in process 1; foo = 1
in process 0; foo = 1
in process 2; foo = 1
in process 3; foo = 1
in process 4; foo = 1
in process 5; foo = 1
=> [2, 2, 2, 2, 2, 2]
But isolation: true
doesn't seem to have the desired effect on rand
. Still don't understand what's going on there.
irb(main):032:0> srand(1);
irb(main):033:0* Parallel.map([0, 1], in_processes: 2) { |i| puts "in process #{i}; rand => #{rand}" }
in process 0; rand => 0.6837528723167413
in process 1; rand => 0.1469087219402977
=> [nil, nil]
irb(main):034:0> srand(1);
irb(main):035:0* Parallel.map([0, 1], in_processes: 2) { |i| puts "in process #{i}; rand => #{rand}" }
in process 0; rand => 0.7906373908366543
in process 1; rand => 0.8807214141308389
=> [nil, nil]
Don't use rand()
which depends on global state. Instead use SecureRandom
or if you need predictable sequences, Random
:
seed = 1
generators = Array.new(2) { Random.new(seed) }
Parallel.map([0, 1], in_processes: 2) do |i|
puts "in process #{i}; rand => #{generators[i].rand}"
end
This gives consistent output:
in process 1; rand => 0.417022004702574
in process 0; rand => 0.417022004702574
This is just one more reason why you shouldn't use rand()
.