When adding multiple threads to a program, care must be taken to ensure any shared data amongst the threads is synchronized properly; without the synchronization, it is possible for a data race to occur. A data race is a condition in which multiple threads try and read and write the same memory location (i.e. variable) at the same time.
To allow a greater amount of flexibility with your code and the library, there are a few synchronization classes each serving a different purpose with different idioms:
It should be noted that most of the lock types are inherently thread safe (except where noted below). This is simply due to the locking nature of the classes and how they have been designed.
Basic Locks (Mutex)
The first type is the
The
It should be noted that both the
Semaphores
Semaphore objects in Omni differ from mutex types in that they are not recursive, additionally semaphores can have multiple locks on them (i.e. multiple "owners") but none of the locking types (mutex or semaphore) will "auto" unlock on destruction. As with the mutex, semaphore types cannot have active waits on destruction, in other words, you cannot call
The
The
A recursive lock allows a thread to call
A non-recursive lock will cause a dead-lock in a situation where one can happen, for example:
In this code,
Spin Locks and Waits
Mutex and Semaphore objects can be quite complex and add additional computational
The
The
The
Each spin lock/wait does not care about thread ownership or if it is
Dead-locks can occur on spin locks but not in the same way as they can with a
In this code,
The Conditional
The
As with the
Scoped Locks
None of the locking types mentioned allow for an "auto" unlock, that is, when an object goes out of scope it does not automatically unlock/release the object. To this, if you wish automatically lock and unlock the different types of objects, you can use one of the auto locking/scoped lock template classes to achieve this.
Both the
The difference between the 2 classes is that
Since these are template functions, types that have member functions of
Example
To allow a greater amount of flexibility with your code and the library, there are a few synchronization classes each serving a different purpose with different idioms:
omni::sync::basic_lockomni::sync::mutexomni::sync::binary_semaphoreomni::sync::conditionalomni::sync::semaphoreomni::sync::spin_lockomni::sync::spin_waitomni::sync::safe_spin_waitomni::sync::auto_lock<T>omni::sync::scoped_lock<T>It should be noted that most of the lock types are inherently thread safe (except where noted below). This is simply due to the locking nature of the classes and how they have been designed.
Basic Locks (Mutex)
The first type is the
omni::sync::basic_lock which is a basic wrapper class for an omni::sync::mutex_t and maintains a simple lock count to keep track if the underlying mutex is still locked on destruction . The basic_lock is, as the name implies, a basic synchronization locking mechanism that allows multi-threaded code to act on data in a thread-safe manner. The basic_lock does not maintain thread ownership or anything beyond if it is still in a locked state on destruction (which would signal a dead-lock condition occurred).The
omni::sync::mutex class is similar to the basic_lock in that it wraps an omni::sync::mutex_t and maintains a lock count for dead-lock conditions, if the OMNI_SAFE_MUTEX is defined, it maintains another internal mutex_t to give thread-safe access to the underlying lock count and handles. Additionally, and most importantly, the mutex class can maintain thread ownership if OMNI_MUTEX_OWNER is defined; it is undefined behavior if unlock is called on a mutex from a thread that does not own the mutex handle, for example:omni::sync::mutex mtx; omni::sync::basic_lock block; void thread1() { /* this is undefined since no ownership is maintained, so this call might succeed but it is implementation defined as to what actually "happens" (i.e. success or crash) */ block.unlock(); // this will generate an error because of the undefined nature mtx.unlock(); } int main() { mtx.lock(); // main thread "owns" the mutex block.lock(); // main thread "owns" the lock omni::sync::thread t1(&thread1); t1.start(); t1.join(); return 0; }
omni::sync::basic_lock and omni::sync::mutex are recursive locks, so a thread that calls lock twice without calling unlock will not result in an immediate dead-lock; care should be taken, however, as a dead-lock situation can still arise if unlock is not called an equal number of times as lock is.Semaphores
Semaphore objects in Omni differ from mutex types in that they are not recursive, additionally semaphores can have multiple locks on them (i.e. multiple "owners") but none of the locking types (mutex or semaphore) will "auto" unlock on destruction. As with the mutex, semaphore types cannot have active waits on destruction, in other words, you cannot call
lock without also calling unlock on any synchronization type.The
omni::sync::semaphore is a non-recursive lock that allows multiple waits to call the object. As with a mutex, the "owner" of the lock will be allowed to execute and those waiting for the lock will block until said lock is free. With a mutex you can only have 1 owner, and thus only 1 lock on the object at a time, but with a semaphore, you can have multiple "owners" and thus multiple lines of code executing while others wait. The
omni::sync::binary_semaphore is a non-recursive lock and is similar to a mutex in that only 1 "owner" can have the lock at any given time (vs. a normal omni::sync::semaphore which can have multiple).A recursive lock allows a thread to call
lock on an object multiple times without causing a dead-lock, for example:omni::sync::mutex mtx; int main() { // wont dead-lock for (int i = 0; i < 10; ++i) { mtx.lock(); } std::cout << "here!" << std::endl; // but must call unlock as many times as we have called locked for (int i = 0; i < 10; ++i) { mtx.unlock(); } return 0; }
omni::sync::binary_semaphore binsem; // 1 owner omni::sync::semaphore sem; // default of 5 "owners" int main() { // main thread "owns" the binary lock binsem.wait(); // main thread "owns" all available wait slots for (int i = 0; i < 5; ++i) { sem.wait(); } binsem.wait(); // dead-lock! // or sem.wait(); // dead-lock! std::cout << "This will never display" << std::endl; return 0; }
wait is called twice on the binary_semaphore without release ever being called. This causes a dead-lock since the binary_semaphore is not recursive. As well, the semaphore has a default wait slot of 5, so in this code all available slots are taken in the for loop causing any additional calls to wait on the semaphore to dead-lock since release was never called at least 1 time on the semaphore.Spin Locks and Waits
Mutex and Semaphore objects can be quite complex and add additional computational
time that might not always be ideal. Sometimes small waits are all that are needed and to this Omni has 3 types of spin wait types.The
omni::sync::spin_lock is a basic interlocked exchange type that calls the platform specific API for a spin lock type (which are usually kernel objects that are efficiently designed for this type of wait).The
omni::sync::spin_wait is a basic boolean check in a tight loop with a OMNI_THREAD_YIELD call to not consume 100% CPU. Since this is a basic boolean check, it is not thread-safe and cannot be guaranteed in a multi-threaded environment to yield accurately, that is, if you call one of the wait functions on the spin_wait object and happen to check if it signalled on another thread at the "same" time, it cannot be guaranteed that the result returned was not over-written by the other thread.The
omni::sync::safe_spin_wait is the same as a omni::sync::spin_wait except it has a omni::sync::mutex_t object to keep the underlying boolean lock thread-safe, guaranteeing its results.Each spin lock/wait does not care about thread ownership or if it is
locked on destruction as that is not what they are intended for. These types of synchronization primitives are intended for a small waits and thus need to be used accordingly. Spin locks and waits are similar to a "flood gate" type of scenario where all threads that have called one of the wait functions will block until signal is called, in which case all threads will continue and non will block on the spin object until it is reset accordingly.Dead-locks can occur on spin locks but not in the same way as they can with a
binary_semaphore type. For example:omni::sync::spin_wait obj; // thread 1 obj.wait(); obj.wait(); // thread 2 obj.signal();
thread 1 will wait until the object is signalled, then after it is signalled, it will call wait again, but depending on the spin lock type and if it is reset or not, the thread will just continue (i.e. not wait). If the spin object had been reset and never signalled again, then a dead-lock condition would occur.The Conditional
The
omni::sync::conditional is a locking type that allows an event based locking mechanism to occur. When a conditional is utilized, all threads that have called wait on the object will block until either signal or broadcast is called. If broadcast is called, all threads that have blocked until that time will be released and signal is reset. If signal is called, vs. the broadcast function, only a single thread will be released according to the thread scheduling policy of the platform (usually in the order called to wait).As with the
mutex and semaphore objects, a conditional type cannot have active waits when the object is being destroyed or an exception occurs. And like the spin lock/wait types, a dead-lock can occur if wait is called multiple times in a row without being signalled and/or reset.Scoped Locks
None of the locking types mentioned allow for an "auto" unlock, that is, when an object goes out of scope it does not automatically unlock/release the object. To this, if you wish automatically lock and unlock the different types of objects, you can use one of the auto locking/scoped lock template classes to achieve this.
Both the
omni::sync::auto_lock and the omni::sync::scoped_lock operate on types that have member functions of lock and unlock to which they call lock when the auto lock object is created and call unlock when the object is destroyed, effectively giving a scoped locking mechanism.The difference between the 2 classes is that
auto_lock validates the handle pointer passed in and will throw an exception if the pointer is invalid and the scoped_lock does no such checks and instead just directly calls lock and unlock on the underlying handle.Since these are template functions, types that have member functions of
lock and unlock (like the std::mutex in C++11) will work with these functions.Example
#include <omnilib> static omni::sync::semaphore sem; // default 5 owners max static omni::sync::spin_wait spin; static omni::sync::basic_lock block; static std::size_t count; static void increment() { omni::sync::scoped_lock<omni::sync::basic_lock> alock(&block); ++count; } static void decrement() { omni::sync::scoped_lock<omni::sync::basic_lock> alock(&block); --count; if (count == 0) { spin.signal(); } } class Obj1 { public: Obj1() : m_val(42), m_mtx(), m_cond() {} Obj1(int val) : m_val(val), m_mtx(), m_cond() {} int get_val() { omni::sync::scoped_lock<omni::sync::mutex> alock(&this->m_mtx); return this->m_val; } void print_val() { this->m_mtx.lock(); std::cout << this->m_val << std::endl; this->m_mtx.unlock(); } int set_val(int val) { omni::sync::scoped_lock<omni::sync::mutex> alock(&this->m_mtx); this->m_val = val; return this->m_val; } void thread_func() { increment(); sem.wait(); this->m_cond.wait(); this->print_val(); sem.release(); decrement(); } void thread_func2() { increment(); sem.wait(); this->m_cond.wait(); this->set_val(10); this->print_val(); sem.release(); decrement(); } void signal_all() { this->m_cond.broadcast(); } private: int m_val; omni::sync::mutex m_mtx; omni::sync::conditional m_cond; }; int main(int argc, char* argv[]) { Obj1 obj1(100); omni::sync::thread t1(omni::sync::bind<Obj1, &Obj1::thread_func2>(obj1)); omni::sync::thread t2(omni::sync::bind<Obj1, &Obj1::thread_func2>(obj1)); for (int i = 0; i < 10; ++i) { omni::sync::create_basic_thread<Obj1, &Obj1::thread_func>(obj1); } t1.start(); t2.start(); obj1.set_val(obj1.get_val() + 10); omni::sync::sleep(1000); obj1.signal_all(); spin.sleep_wait(); t1.join(); t2.join(); std::cout << "leaving" << std::endl; return 0; }