Synchronization Locks
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:

omni::sync::basic_lock
omni::sync::mutex
omni::sync::binary_semaphore
omni::sync::conditional
omni::sync::semaphore
omni::sync::spin_lock
omni::sync::spin_wait
omni::sync::safe_spin_wait
omni::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;
}
It should be noted that both the 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;
}
A non-recursive lock will cause a dead-lock in a situation where one can happen, for example:
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;
}
In this code, 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();
In this code, 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;
}