// // MIT License // Copyright (c) 2020 Jonathan R. Madsen // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal // in the Software without restriction, including without limitation the rights // to use, copy, modify, merge, publish, distribute, sublicense, and // copies of the Software, and to permit persons to whom the Software is // furnished to do so, subject to the following conditions: // The above copyright notice and this permission notice shall be included in // all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED // "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT // LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR // PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN // ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // // // --------------------------------------------------------------- // Tasking class header file // /// Class Description: /// /// This class provides a mechanism to create a mutex and locks/unlocks it. /// Can be used by applications to implement in a portable way a mutexing logic. /// Usage Example: /// /// #include "Threading.hh" /// #include "AutoLock.hh" /// /// /// defined somewhere -- static so all threads see the same mutex /// static Mutex aMutex; /// /// /// somewhere else: /// /// The AutoLock instance will automatically unlock the mutex when it /// /// goes out of scope. One typically defines the scope within { } if /// /// there is thread-safe code following the auto-lock /// /// { /// AutoLock l(&aMutex); /// ProtectedCode(); /// } /// /// UnprotectedCode(); /// /// /// When ProtectedCode() is calling a function that also tries to lock /// /// a normal AutoLock + Mutex will "deadlock". In other words, the /// /// the mutex in the ProtectedCode() function will wait forever to /// /// acquire the lock that is being held by the function that called /// /// ProtectedCode(). In this situation, use a RecursiveAutoLock + /// /// RecursiveMutex, e.g. /// /// /// defined somewhere -- static so all threads see the same mutex /// static RecursiveMutex aRecursiveMutex; /// /// /// this function is sometimes called directly and sometimes called /// /// from SomeFunction_B(), which also locks the mutex /// void SomeFunction_A() /// { /// /// when called from SomeFunction_B(), a Mutex + AutoLock will /// /// deadlock /// RecursiveAutoLock l(&aRecursiveMutex); /// /// do something /// } /// /// void SomeFunction_B() /// { /// /// { /// RecursiveAutoLock l(&aRecursiveMutex); /// SomeFunction_A(); /// } /// /// UnprotectedCode(); /// } /// /// /// --------------------------------------------------------------- /// Author: Andrea Dotti (15 Feb 2013): First Implementation /// /// Update: Jonathan Madsen (9 Feb 2018): Replaced custom implementation /// with inheritance from C++11 unique_lock, which inherits the /// following member functions: /// /// - unique_lock(unique_lock&& other) noexcept; /// - explicit unique_lock(mutex_type& m); /// - unique_lock(mutex_type& m, std::defer_lock_t t) noexcept; /// - unique_lock(mutex_type& m, std::try_to_lock_t t); /// - unique_lock(mutex_type& m, std::adopt_lock_t t); /// /// - template /// unique_lock(mutex_type& m, /// const std::chrono::duration& /// timeout_duration); /// /// - template /// unique_lock(mutex_type& m, /// const std::chrono::time_point& timeout_time); /// /// - void lock(); /// - void unlock(); /// - bool try_lock(); /// /// - template /// bool try_lock_for(const std::chrono::duration&); /// /// - template /// bool try_lock_until(const std::chrono::time_point&); /// /// - void swap(unique_lock& other) noexcept; /// - mutex_type* release() noexcept; /// - mutex_type* mutex() const noexcept; /// - bool owns_lock() const noexcept; /// - explicit operator bool() const noexcept; /// - unique_lock& operator=(unique_lock&& other); /// /// --------------------------------------------------------------- /// /// Note that AutoLock is defined also for a sequential Tasking build but below /// regarding implementation (also found in Threading.hh) /// /// /// NOTE ON Tasking SERIAL BUILDS AND MUTEX/UNIQUE_LOCK /// ================================================== /// /// Mutex and RecursiveMutex are always C++11 std::mutex types /// however, in serial mode, using MUTEXLOCK and MUTEXUNLOCK on these /// types has no effect -- i.e. the mutexes are not actually locked or unlocked /// /// Additionally, when a Mutex or RecursiveMutex is used with AutoLock /// and RecursiveAutoLock, respectively, these classes also suppressing /// the locking and unlocking of the mutex. Regardless of the build type, /// AutoLock and RecursiveAutoLock inherit from std::unique_lock /// and std::unique_lock, respectively. This means /// that in situations (such as is needed by the analysis category), the /// AutoLock and RecursiveAutoLock can be passed to functions requesting /// a std::unique_lock. Within these functions, since std::unique_lock /// member functions are not virtual, they will not retain the dummy locking /// and unlocking behavior /// --> An example of this behavior can be found below /// /// Jonathan R. Madsen (February 21, 2018) /// /*** //======================================================================================// typedef std::unique_lock unique_lock_t; // functions for casting AutoLock to std::unique_lock to demonstrate // that AutoLock is NOT polymorphic void as_unique_lock(unique_lock_t* lock) { lock->lock(); } void as_unique_unlock(unique_lock_t* lock) { lock->unlock(); } //======================================================================================// void run(const uint64_t& n) { // sync the threads a bit std::this_thread::sleep_for(std::chrono::milliseconds(10)); // get two mutexes to avoid deadlock when l32 actually locks AutoLock l32(TypeMutex(), std::defer_lock); AutoLock l64(TypeMutex(), std::defer_lock); // when serial: will not execute std::unique_lock::lock() because // it overrides the member function l32.lock(); // regardless of serial or MT: will execute std::unique_lock::lock() // because std::unique_lock::lock() is not virtual as_unique_lock(&l64); std::cout << "Running iteration " << n << "..." << std::endl; } //======================================================================================// // execute some work template void exec(uint64_t n) { // get two mutexes to avoid deadlock when l32 actually locks AutoLock l32(TypeMutex(), std::defer_lock); AutoLock l64(TypeMutex(), std::defer_lock); std::vector threads(n, nullptr); for(uint64_t i = 0; i < n; ++i) { threads[i] = new thread_type(); *(threads[i]) = std::move(thread_type(run, i)); } // when serial: will not execute std::unique_lock::lock() because // it overrides the member function l32.lock(); // regardless of serial or MT: will execute std::unique_lock::lock() // because std::unique_lock::lock() is not virtual as_unique_lock(&l64); std::cout << "Joining..." << std::endl; // when serial: will not execute std::unique_lock::unlock() because // it overrides the member function l32.unlock(); // regardless of serial or MT: will execute std::unique_lock::unlock() // because std::unique_lock::unlock() is not virtual as_unique_unlock(&l64); // NOTE ABOUT UNLOCKS: // in MT, commenting out either // l32.unlock(); // or // as_unique_unlock(&l64); // creates a deadlock; in serial, commenting out // as_unique_unlock(&l64); // creates a deadlock but commenting out // l32.unlock(); // does not // clean up and join for(uint64_t i = 0; i < n; ++i) { threads[i]->join(); delete threads[i]; } threads.clear(); } //======================================================================================// int main() { print_threading(); uint64_t n = 30; std::cout << "\nRunning with real threads...\n" << std::endl; exec(n); std::cout << "\nRunning with fake threads...\n" << std::endl; exec(n); } ***/ #pragma once #include "PTL/ConsumeParameters.hh" #include "PTL/Types.hh" #include #include #include #include namespace PTL { // Note: Note that TemplateAutoLock by itself is not thread-safe and // cannot be shared among threads due to the locked switch // template class TemplateAutoLock : public std::unique_lock { public: //------------------------------------------------------------------------// // Some useful typedefs //------------------------------------------------------------------------// using unique_lock_t = std::unique_lock; using this_type = TemplateAutoLock; using mutex_type = typename unique_lock_t::mutex_type; public: //------------------------------------------------------------------------// // STL-consistent reference form constructors //------------------------------------------------------------------------// // reference form is consistent with STL lock_guard types // Locks the associated mutex by calling m.lock(). The behavior is // undefined if the current thread already owns the mutex except when // the mutex is recursive explicit TemplateAutoLock(mutex_type& _mutex) : unique_lock_t(_mutex, std::defer_lock) { // call termination-safe locking. if serial, this call has no effect _lock_deferred(); } // Tries to lock the associated mutex by calling // m.try_lock_for(_timeout_duration). Blocks until specified // _timeout_duration has elapsed or the lock is acquired, whichever comes // first. May block for longer than _timeout_duration. template TemplateAutoLock(mutex_type& _mutex, const std::chrono::duration& _timeout_duration) : unique_lock_t(_mutex, std::defer_lock) { // call termination-safe locking. if serial, this call has no effect _lock_deferred(_timeout_duration); } // Tries to lock the associated mutex by calling // m.try_lock_until(_timeout_time). Blocks until specified _timeout_time has // been reached or the lock is acquired, whichever comes first. May block // for longer than until _timeout_time has been reached. template TemplateAutoLock(mutex_type& _mutex, const std::chrono::time_point& _timeout_time) : unique_lock_t(_mutex, std::defer_lock) { // call termination-safe locking. if serial, this call has no effect _lock_deferred(_timeout_time); } // Does not lock the associated mutex. TemplateAutoLock(mutex_type& _mutex, std::defer_lock_t _lock) noexcept : unique_lock_t(_mutex, _lock) {} // Tries to lock the associated mutex without blocking by calling // m.try_lock(). The behavior is undefined if the current thread already // owns the mutex except when the mutex is recursive. TemplateAutoLock(mutex_type& _mutex, std::try_to_lock_t _lock) : unique_lock_t(_mutex, _lock) {} // Assumes the calling thread already owns m TemplateAutoLock(mutex_type& _mutex, std::adopt_lock_t _lock) : unique_lock_t(_mutex, _lock) {} public: //------------------------------------------------------------------------// // Backwards compatibility versions (constructor with pointer to mutex) //------------------------------------------------------------------------// TemplateAutoLock(mutex_type* _mutex) : unique_lock_t(*_mutex, std::defer_lock) { // call termination-safe locking. if serial, this call has no effect _lock_deferred(); } TemplateAutoLock(mutex_type* _mutex, std::defer_lock_t _lock) noexcept : unique_lock_t(*_mutex, _lock) {} TemplateAutoLock(mutex_type* _mutex, std::try_to_lock_t _lock) : unique_lock_t(*_mutex, _lock) {} TemplateAutoLock(mutex_type* _mutex, std::adopt_lock_t _lock) : unique_lock_t(*_mutex, _lock) {} private: // helpful macros #define _is_stand_mutex(Tp) (std::is_same::value) #define _is_recur_mutex(Tp) (std::is_same::value) #define _is_other_mutex(Tp) (!_is_stand_mutex(Tp) && !_is_recur_mutex(Tp)) template ::type = 0> std::string GetTypeString() { return "AutoLock"; } template ::type = 0> std::string GetTypeString() { return "AutoLock"; } template ::type = 0> std::string GetTypeString() { return "AutoLock"; } // pollution is bad #undef _is_stand_mutex #undef _is_recur_mutex #undef _is_other_mutex //========================================================================// // NOTE on _lock_deferred(...) variants: // a system_error in lock means that the mutex is unavailable // we want to throw the error that comes from locking an unavailable // mutex so that we know there is a memory leak // if the mutex is valid, this will hold until the other thread // finishes // sometimes certain destructors use locks, this isn't an issue unless // the object is leaked. When this occurs, the application finalization // (i.e. the real or implied "return 0" part of main) will call destructors // on Tasking object after some static mutex variables are deleted, leading // to the error code (typically on Clang compilers): // libc++abi.dylib: terminating with uncaught exception of type // std::__1::system_error: mutex lock failed: Invalid argument // this function protects against this failure until such a time that // these issues have been resolved //========================================================================// // standard locking inline void _lock_deferred() { try { this->unique_lock_t::lock(); } catch(std::system_error& e) { PrintLockErrorMessage(e); } } //========================================================================// // Tries to lock the associated mutex by calling // m.try_lock_for(_timeout_duration). Blocks until specified // _timeout_duration has elapsed or the lock is acquired, whichever comes // first. May block for longer than _timeout_duration. template void _lock_deferred(const std::chrono::duration& _timeout_duration) { try { this->unique_lock_t::try_lock_for(_timeout_duration); } catch(std::system_error& e) { PrintLockErrorMessage(e); } } //========================================================================// // Tries to lock the associated mutex by calling // m.try_lock_until(_timeout_time). Blocks until specified _timeout_time has // been reached or the lock is acquired, whichever comes first. May block // for longer than until _timeout_time has been reached. template void _lock_deferred(const std::chrono::time_point& _timeout_time) { try { this->unique_lock_t::try_lock_until(_timeout_time); } catch(std::system_error& e) { PrintLockErrorMessage(e); } } //========================================================================// // the message for what mutex lock fails due to deleted static mutex // at termination void PrintLockErrorMessage(std::system_error& e) { // use std::cout/std::endl to avoid include dependencies using std::cout; using std::endl; // the error that comes from locking an unavailable mutex #if defined(VERBOSE) cout << "Non-critical error: mutex lock failure in " << GetTypeString() << ". " << "If the app is terminating, Tasking failed to " << "delete an allocated resource and a Tasking destructor is " << "being called after the statics were destroyed. \n\t--> " << "Exception: [code: " << e.code() << "] caught: " << e.what() << std::endl; #else ConsumeParameters(e); #endif } }; // -------------------------------------------------------------------------- // // // Use the non-template types below: // - AutoLock with Mutex // - RecursiveAutoLock with RecursiveMutex // // -------------------------------------------------------------------------- // using AutoLock = TemplateAutoLock; using RecursiveAutoLock = TemplateAutoLock; } // namespace PTL