-
-
Notifications
You must be signed in to change notification settings - Fork 375
lib: Make random number generation thread safe #6480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 1 commit
5ccafac
2c4592d
c29147e
f057b67
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -3,12 +3,12 @@ | |||||
| * | ||||||
| * \brief GIS Library - Pseudo-random number generation | ||||||
| * | ||||||
| * (C) 2014 by the GRASS Development Team | ||||||
| * (C) 2014, 2025 by the GRASS Development Team | ||||||
wenzeslaus marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||
| * | ||||||
| * This program is free software under the GNU General Public License | ||||||
| * (>=v2). Read the file COPYING that comes with GRASS for details. | ||||||
| * | ||||||
| * \author Glynn Clements | ||||||
| * \authors Glynn Clements, Maris Nartiss (thread safety) | ||||||
| */ | ||||||
|
|
||||||
| #include <stdio.h> | ||||||
|
|
@@ -19,6 +19,10 @@ | |||||
| #include <grass/gis.h> | ||||||
| #include <grass/glocale.h> | ||||||
|
|
||||||
| #ifdef HAVE_PTHREAD | ||||||
| #include <pthread.h> | ||||||
| #endif | ||||||
|
|
||||||
| #ifdef HAVE_GETTIMEOFDAY | ||||||
| #include <sys/time.h> | ||||||
| #else | ||||||
|
|
@@ -41,22 +45,36 @@ static const uint32 b0 = 0xB; | |||||
|
|
||||||
| static int seeded; | ||||||
|
|
||||||
| #ifdef HAVE_PTHREAD | ||||||
| static pthread_mutex_t lrand48_mutex = PTHREAD_MUTEX_INITIALIZER; | ||||||
| #endif | ||||||
|
|
||||||
| #define LO(x) ((x) & 0xFFFFU) | ||||||
| #define HI(x) ((x) >> 16) | ||||||
|
|
||||||
| /*! | ||||||
| * \brief Seed the pseudo-random number generator | ||||||
| * | ||||||
| * \param[in] seedval 32-bit integer used to seed the PRNG | ||||||
| * | ||||||
| * This function is thread-safe. | ||||||
|
||||||
| * This function is thread-safe. | |
| * This function is thread-safe (if threading is enabled, i.e., `HAVE_PTHREAD` is defined). |
or: ...if pthreads is available,...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Disagree. This makes no sense. Of course there is no thread safety when code is compiled without threads! This is a programmer documentation that is meant to signal other developers that it is safe to call this function from threaded code. Attempts to compile core GRASS without pthreads and calling from a module with pthreads should not be considered in documentation of each function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Calling this from multiple threads will reset the global state. So, while the locking will prevent creation of a broken global state and it will prevent from reading a half-baked state, it will not work as expected. If you call this from multiple threads, they will replace the previous thread random state. Only the main thread should call this function and typically would call it once unless resetting the seed - that should be documented.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are correct. I added a note to the documentation. Mutex guards should remain in place, but seed initialization always should be called only from main process/single thread. I called multiple times, it would result in "last wins" state.
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This approach only locks the advancing, so there will be no broken state. However, consider this scenario:
- in Thread 1, mrand48 calls next
- in Thread 1, next locks and does the advancing
- in Thread 1, next unlocks and returns
- in Thread 2, mrand48 calls next
- in Thread 2, next locks and does the advancing
- in Thread 2, mrand48 reads the static variable values
- in Thread 1, mrand48 reads the static variable values which are the same ones as in Thread 2, so the Thread 1 values were never used
Alternatively, Thread 1 can be reading the half-baked state which is being overwritten by Thread 2.
Either the locking needs to happen at the level of mrand48-type functions, i.e., include both reading and writing (or lock separately from writing), or G__next (or its wrapper) needs to return the values rather than rely on the global state.
I prefer the solution which limits the global state and uses explicit return values (or even passes a state around), so my choice would be to return (pass) values from G__next. (In a more substantial rewrite scenario, an explicit "context" object would make the sharing of state explicit.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You are spot-on about too narrow scope of mutex. I changed code to include also consumption of state inside the mutex.
The new implementation makes RNG serial – independently from number of threads calling those functions, number sequence always will be the same.
If G__next would return a state object, each thread would have its own state and, with an identical seed, result in an identical stream of random numbers – not good at all. If state is passed around between calls (e.g. G_lrand48(struct state)) we are even worse as then locking/sharing of state would have to be implemented by each caller itself and we still are facing thread synchronization problem. Did I miss something?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new implementation makes RNG serial – independently from number of threads calling those functions, number sequence always will be the same.
Awesome, thanks. That's what I had in mind. Least changes in terms of code structure, although most mutex additions.
If
G__nextwould return a state object, each thread would have its own state and, with an identical seed, result in an identical stream of random numbers – not good at all.
This requires starting the different states with different seeds. This works well for different stochastic replicates of the same simulation. It would require additional decisions on how to behave within one raster.
If state is passed around between calls (e.g.
G_lrand48(struct state)) we are even worse as then locking/sharing of state would have to be implemented by each caller itself and we still are facing thread synchronization problem.
The mutex would have to be part of that state and access would have to be only through functions, basically an OOP encapsulation. In other words, every variable from the library would have to go to the state.
Edit: That is if we want the locking for the states, but my original idea was that independent states don't need locking because there is nothing shared between them, and they are simply not suitable to be used across threads. In other words, a not thread-safe API meant to be used with different instances in different threads which would be then a thread-safe usage pattern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Edit: That is if we want the locking for the states, but my original idea was that independent states don't need locking because there is nothing shared between them, and they are simply not suitable to be used across threads. In other words, a not thread-safe API meant to be used with different instances in different threads which would be then a thread-safe usage pattern.
You mean – thread-local storage? I discarded this idea because outcome is totally not reproducible. To make things worse – each thread would have to have its own unique seed value otherwise all of them would produce identical number sequences.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thread-local storage - whatever the naming, I meant passing a separate state object to each thread.
Reproducible? If each thread gets its own seed, it generates its own sequence. For a given number of threads, there is the same set numbers generated. If each thread is assigned the same rows every time it runs, the raster gets the same numbers on each position.
Each thread getting its own seed - yes, that's what I'm saying would be needed to get reproducible results. For a raster, it further depends on how threads are assigned rows, if that depends on thread's performance or it is determined ahead of time based on the number of threads (I think that's what we have in the r.mapcalc code). To make the results not only reproducible for seed+nprocs, but for seed and any nprocs number, we would have to have a separate seed for each row so that it is independent on number of threads (so each row seeded with seed plus row number).
In any case, this something extra. My point here was the missing mutex.
However, the discussion also connects to the issue of the currently generated sequence. What throws me off is your mention of discarding the idea of thread-local storage because outcome is not reproducible. In your current implementation, the number sequence is the same for every run and number of threads, but how they are distributed in practice within a raster depends on the order execution of the threads, i.e., it may differ for every run even with the same number of threads.
So, even your current implementation does not offer reproducibility. Your comment about guarantees, outcomes, and reproducibility suggests we are in agreement on that. Is that correct? I just want to be sure we are clear about what to expect from the code now.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All this should be replaced by the addition of
check_target(Threads::Threads HAVE_PTHREAD)to this file:grass/cmake/modules/CheckDependentLibraries.cmake
Line 222 in 76ee406