================================================== Controlling concurrent access to a shared resource [TOC] ================================================== In our previous examples we followed the fork and join pattern in a manner where the threads operated strictly independently from each other, i.e. none of the threads attempted to access directly or indirectly shared resources or data structures. To uphold this, we were enforced to create individual pseudo-number generators for each thread with different seed values. This does not come cheap. On our computer Thales, the generation of a single uniformely distributed pseudo-random number using `std::mt19937` takes in average 8.4 nano seconds whereas the construction and preparation of a `std::mt19937` object takes about 9 micro seconds. You need even more time when you want more entropy than just from one 32-bit value. Mutex variables =============== Examples like this might lead us to the question whether concurrent accesses to a shared resource like a pseudo-random generator are possible. The thread interfaces of POSIX and C++11 provide so-called mutex variables for this purpose where mutex is a shortname for _mutual exclusion_. Mutex variables can be used to make sure that at maximum just one thread accesses a shared resource. Other threads that want to access the resource at the same time have to wait until the other thread has finished its access. Mutex variables of the type `std::mutex` out of `` can be easily used: * The `lock` method suspends the invoking thread until we have acquired exclusive access to the associated resource. * The `unlock` method releases the mutex variable, allowing other threads to access the resource. The access of a shared pseudo-random number generator could be wrapped as shown in following example: ---- CODE (type=cpp) ---------------------------------------------------------- struct MyRandomGenerator { MyRandomGenerator() : mt(std::random_device()()), uniform(-100, 100) { } double gen() { mutex.lock(); double val = uniform(mt); mutex.unlock(); return val; } std::mutex mutex; std::mt19937 mt; std::uniform_real_distribution uniform; }; ------------------------------------------------------------------------------- The `gen` method assures exclusive access using `mutex.lock()`, retrieves a value from the pseudo-random generator, and finally releases its exclusive access to the resource. However, the variables of the `MyRandomGenerator` class are publically accessible, i.e. anyone is free to access `mt` directly without using the `gen` method or even without getting exclusive access first through `mutex`. So far, we were not bothered when classes provided public access to all of its internal variables. In this case, however, we would like to make sure to keep the variables `mutex`, `mt` and `uniform` private such that `gen` has to be used. This is possible by declaring these variables `private`: ---- CODE (type=cpp) ---------------------------------------------------------- struct MyRandomGenerator { MyRandomGenerator() : mt(std::random_device()()), uniform(-100, 100) { } double gen() { mutex.lock(); double val = uniform(mt); mutex.unlock(); return val; } private: std::mutex mutex; std::mt19937 mt; std::uniform_real_distribution uniform; }; ------------------------------------------------------------------------------- This code is still not safe as we are unfortunately unable to assure that `mutex.unlock()` is guaranteed to be called after `mutex.lock()`. Is this actually possible in this case where `mutex.lock()` and `mutex.unlock()` are so nicely paired within a method? Unfortunately, this can indeed happen when the invocation of `uniform(mt)` leads to an exception that causes the stack to be unwound until an exception handler is found. In the course of this stack unwinding the `gen()` method invocation is abandoned and `mutex.unlock()` gets never invoked. Because of this problem, the above presented solution is not _exception safe_. The only approach in C++ to achieve exception safeness works on base of destructors. As C++ guarantees that even in case of an unwound stack all destructors are properly called we need to move the invocation of `mutex.unlock()` to a destructor. Hence, we need again to apply the RAII principle (_resource acquisition is initialization_) where an object that is responsible for the mutex variable * invokes `mutex.lock()` during its construction, and * `mutex.unlock()` when it is destructed. Exercise ======== Write a simple and minimalistic RAII class as described for mutex variables, use this within the `MyRandomGenerator` example, and test it by applying this solution in `random_init8.cpp` which is similar to the one we used on last Monday but uses _UniformSlices_ from ``. The header-only library for this session is available in the directory _/home/numerik/pub/hpc/ws18/session16_ on our hosts. :import:session15/random_init8.cpp ---- SHELL (path=session16) --------------------------------------------------- g++ -O3 -g -I/home/numerik/pub/hpc/ws18/session16 -std=c++17 -o random_init8 random_init8.cpp ./random_init8 ------------------------------------------------------------------------------- :navigate: up -> doc:index next -> doc:session16/page02