Repository: cupy/cupy

Issue:
get_random_state() is not thread-safe
In the code below, the seed should be set to 10 no matter what order these threads are executed.

**Code**:
```
import threading
import random
import cupy

SEED = 10

def func_seed():
    cupy.random.seed(SEED)

def func_get_random_state():
    cupy.random.get_random_state()

def test():
    procs = [
        threading.Thread(target=func_seed),
        threading.Thread(target=func_get_random_state),
        threading.Thread(target=func_get_random_state),
        threading.Thread(target=func_get_random_state),
        threading.Thread(target=func_get_random_state),
        threading.Thread(target=func_get_random_state),
        threading.Thread(target=func_get_random_state),
    ]
    random.shuffle(procs)

    for p in procs:
        p.start()
        #p.join()  # this hides the problem

    for p in procs:
        p.join()

    actual = cupy.random.uniform()

    cupy.random.seed(SEED)
    expected = cupy.random.uniform()

    print("Expected: {}".format(expected))
    print("Actual  : {}".format(actual))

test()
```

**Result**:
```
Expected: 0.6320792449063122
Actual  : 0.41662013290707867
```


Discussion:

Resolving patch:
diff --git a/cupy/random/generator.py b/cupy/random/generator.py
--- a/cupy/random/generator.py
+++ b/cupy/random/generator.py
@@ -341,7 +341,6 @@ def get_random_state():
         device.
 
     """
-    global _random_states
     dev = cuda.Device()
     rs = _random_states.get(dev.id, None)
     if rs is None:
@@ -349,7 +348,7 @@ def get_random_state():
         if seed is None:
             seed = os.getenv('CHAINER_SEED')
         rs = RandomState(seed)
-        _random_states[dev.id] = rs
+        rs = _random_states.setdefault(dev.id, rs)
     return rs
 
 


Test patch:
diff --git a/tests/cupy_tests/random_tests/test_generator.py b/tests/cupy_tests/random_tests/test_generator.py
--- a/tests/cupy_tests/random_tests/test_generator.py
+++ b/tests/cupy_tests/random_tests/test_generator.py
@@ -1,6 +1,7 @@
 import mock
 import operator
 import os
+import threading
 import unittest
 
 import numpy
@@ -383,6 +384,35 @@ def test_get_random_state_memoized(self):
         self.assertEqual('expected', rs)
 
 
+@testing.gpu
+class TestGetRandomStateThreadSafe(unittest.TestCase):
+
+    def setUp(self):
+        cupy.random.reset_states()
+
+    def test_thread_safe(self):
+        seed = 10
+        threads = [
+            threading.Thread(target=lambda: cupy.random.seed(seed)),
+            threading.Thread(target=lambda: cupy.random.get_random_state()),
+            threading.Thread(target=lambda: cupy.random.get_random_state()),
+            threading.Thread(target=lambda: cupy.random.get_random_state()),
+            threading.Thread(target=lambda: cupy.random.get_random_state()),
+            threading.Thread(target=lambda: cupy.random.get_random_state()),
+            threading.Thread(target=lambda: cupy.random.get_random_state()),
+        ]
+
+        for t in threads:
+            t.start()
+        for t in threads:
+            t.join()
+
+        actual = cupy.random.uniform()
+        cupy.random.seed(seed)
+        expected = cupy.random.uniform()
+        self.assertEqual(actual, expected)
+
+
 @testing.gpu
 class TestGetRandomState2(unittest.TestCase):