Skip to content

Commit 4b93e6b

Browse files
authored
Merge pull request #74 from bashtage/multithreaded-example
DOC: Add example of multithreaded filling
2 parents 2b70909 + 3e2e560 commit 4b93e6b

File tree

2 files changed

+127
-0
lines changed

2 files changed

+127
-0
lines changed

doc/source/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ New Features
113113
:maxdepth: 2
114114

115115
Using in Parallel Applications <parallel>
116+
Multithreaded Generation <multithreading>
116117
Reading System Entropy <entropy>
117118

118119

doc/source/multithreading.rst

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
Multithreaded Generation
2+
========================
3+
4+
The four core distributions all allow existing arrays to be filled using the
5+
``out`` keyword argument. Existing arrays need to be contiguous and
6+
well-behaved (writable and algined). Under normal circumstances, arrays
7+
created using the common constructors such as ``numpy.empty`` will satisfy
8+
these requirements.
9+
10+
This example makes use of Python 3 ``futures`` to fill an array using multiple
11+
threads. Threads are long-lived so that repeated calls do not require any
12+
additional overheads from thread creation. The undelying PRNG is xorshift2014
13+
which is fast, has a long period and supports using ``jump`` to advange the
14+
state. The random numbers generated are reproducible in the sense that the
15+
same seed will produce the same outputs.
16+
17+
::
18+
19+
import randomstate
20+
import multiprocessing
21+
import concurrent.futures
22+
import numpy as np
23+
24+
class MultithreadedRNG(object):
25+
def __init__(self, n, seed=None, threads=None):
26+
rs = randomstate.prng.xorshift1024.RandomState(seed)
27+
if threads is None:
28+
threads = multiprocessing.cpu_count()
29+
self.threads = threads
30+
31+
self._random_states = [rs]
32+
for _ in range(1, threads):
33+
_rs = randomstate.prng.xorshift1024.RandomState()
34+
rs.jump()
35+
_rs.set_state(rs.get_state())
36+
self._random_states.append(_rs)
37+
38+
self.n = n
39+
self.executor = concurrent.futures.ThreadPoolExecutor(threads)
40+
self.values = np.empty(n)
41+
self.step = np.ceil(n / threads).astype(np.int)
42+
43+
def fill(self):
44+
def _fill(random_state, out, first, last):
45+
random_state.standard_normal(out=out[first:last])
46+
47+
futures = {}
48+
for i in range(self.threads):
49+
args = (_fill, self._random_states[i], self.values, i * self.step, (i + 1) * self.step)
50+
futures[self.executor.submit(*args)] = i
51+
concurrent.futures.wait(futures)
52+
53+
def __del__(self):
54+
self.executor.shutdown(False)
55+
56+
57+
.. ipython:: python
58+
:suppress:
59+
60+
In [1]: import randomstate
61+
....: import multiprocessing
62+
....: import concurrent.futures
63+
....: import numpy as np
64+
....:
65+
....: class MultithreadedRNG(object):
66+
....: def __init__(self, n, seed=None, threads=None):
67+
....: rs = randomstate.prng.xorshift1024.RandomState(seed)
68+
....: if threads is None:
69+
....: threads = multiprocessing.cpu_count()
70+
....: self.threads = threads
71+
....: self._random_states = [rs]
72+
....: for _ in range(1, threads):
73+
....: _rs = randomstate.prng.xorshift1024.RandomState()
74+
....: rs.jump()
75+
....: _rs.set_state(rs.get_state())
76+
....: self._random_states.append(_rs)
77+
....: self.n = n
78+
....: self.executor = concurrent.futures.ThreadPoolExecutor(threads)
79+
....: self.values = np.empty(n)
80+
....: self.step = np.ceil(n / threads).astype(np.int)
81+
....: def fill(self):
82+
....: def _fill(random_state, out, first, last):
83+
....: random_state.standard_normal(out=out[first:last])
84+
....: futures = {}
85+
....: for i in range(self.threads):
86+
....: args = (_fill, self._random_states[i], self.values, i * self.step, (i + 1) * self.step)
87+
....: futures[self.executor.submit(*args)] = i
88+
....: concurrent.futures.wait(futures)
89+
....: def __del__(self):
90+
....: self.executor.shutdown(False)
91+
....:
92+
93+
The multithreaded random number generator can be used to fill an array.
94+
The ``values`` attributes shows the zero-value before the fill and the
95+
random value after.
96+
97+
.. ipython:: python
98+
99+
mrng = MultithreadedRNG(10000000, seed=0)
100+
print(mrng.values[-1])
101+
mrng.fill()
102+
print(mrng.values[-1])
103+
104+
The time required to produce using multiple threads can be compared to
105+
the time required to generate using a single thread.
106+
107+
.. ipython:: python
108+
109+
print(mrng.threads)
110+
%timeit mrng.fill()
111+
112+
113+
The single threaded call directly uses the PRNG.
114+
115+
.. ipython:: python
116+
117+
values = np.empty(10000000)
118+
%timeit randomstate.prng.xorshift1024.standard_normal(out=values)
119+
120+
The gains are substantial and the scaling is reasonable even for large that
121+
are only moderately large. The gains are even larger when compared to a call
122+
that does not use an existing array due to array creation overhead.
123+
124+
.. ipython:: python
125+
126+
%timeit randomstate.prng.xorshift1024.standard_normal(10000000)

0 commit comments

Comments
 (0)